diff --git a/playground/AzureAppService/AzureAppService.AppHost/AppHost.cs b/playground/AzureAppService/AzureAppService.AppHost/AppHost.cs index 36096e351dc..9d03da7982a 100644 --- a/playground/AzureAppService/AzureAppService.AppHost/AppHost.cs +++ b/playground/AzureAppService/AzureAppService.AppHost/AppHost.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + using Aspire.Hosting.Azure; using Azure.Provisioning.Storage; @@ -13,7 +15,7 @@ // Testing kv secret refs var cosmosDb = builder.AddAzureCosmosDB("account") - .RunAsEmulator(c => c.WithLifetime(ContainerLifetime.Persistent)); + .RunAsEmulator(c => c.WithPersistentLifetime()); cosmosDb.AddCosmosDatabase("db"); @@ -24,7 +26,7 @@ var storage = infra.GetProvisionableResources().OfType().Single(); storage.AllowBlobPublicAccess = false; }) - .RunAsEmulator(c => c.WithLifetime(ContainerLifetime.Persistent)); + .RunAsEmulator(c => c.WithPersistentLifetime()); var blobs = storage.AddBlobs("blobs"); // Testing projects diff --git a/playground/AzureContainerApps/AzureContainerApps.AppHost/AppHost.cs b/playground/AzureContainerApps/AzureContainerApps.AppHost/AppHost.cs index 86876845d09..703410c09cf 100644 --- a/playground/AzureContainerApps/AzureContainerApps.AppHost/AppHost.cs +++ b/playground/AzureContainerApps/AzureContainerApps.AppHost/AppHost.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREACADOMAINS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. using Aspire.Hosting.Azure; using Azure.Provisioning.Storage; @@ -18,19 +19,19 @@ // Testing volumes var redis = builder.AddRedis("cache") - .WithLifetime(ContainerLifetime.Persistent) + .WithPersistentLifetime() .WithDataVolume(); // Testing secret outputs var cosmosDb = builder.AddAzureCosmosDB("account") .WithAccessKeyAuthentication() - .RunAsEmulator(c => c.WithLifetime(ContainerLifetime.Persistent)); + .RunAsEmulator(c => c.WithPersistentLifetime()); cosmosDb.AddCosmosDatabase("db"); // Testing a connection string var storage = builder.AddAzureStorage("storage") - .RunAsEmulator(c => c.WithLifetime(ContainerLifetime.Persistent)); + .RunAsEmulator(c => c.WithPersistentLifetime()); var blobs = storage.AddBlobs("blobs"); // Testing docker files diff --git a/playground/AzureServiceBus/ServiceBus.AppHost/AppHost.cs b/playground/AzureServiceBus/ServiceBus.AppHost/AppHost.cs index 7ce1556a856..6004a5037c3 100644 --- a/playground/AzureServiceBus/ServiceBus.AppHost/AppHost.cs +++ b/playground/AzureServiceBus/ServiceBus.AppHost/AppHost.cs @@ -1,6 +1,8 @@ using System.Text.Json.Nodes; using Aspire.Hosting.Azure; +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + var builder = DistributedApplication.CreateBuilder(args); var serviceBus = builder.AddAzureServiceBus("sbemulator"); @@ -34,7 +36,7 @@ serviceBus.RunAsEmulator(configure => configure.WithConfiguration(document => { document["UserConfig"]!["Logging"] = new JsonObject { ["Type"] = "Console" }; -}).WithLifetime(ContainerLifetime.Persistent)); +}).WithPersistentLifetime()); builder.AddProject("worker") .WithReference(queue).WaitFor(queue) diff --git a/playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/AppHost.cs b/playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/AppHost.cs index 70f955c925c..55053847beb 100644 --- a/playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/AppHost.cs +++ b/playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/AppHost.cs @@ -3,6 +3,7 @@ #pragma warning disable AZPROVISION001 // Azure.Provisioning.Network is experimental #pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. using Aspire.Hosting.Azure; using Azure.Provisioning.Network; @@ -49,7 +50,7 @@ privateEndpointsSubnet.AddPrivateEndpoint(queues); var sqlServer = builder.AddAzureSqlServer("sql") - .RunAsContainer(c => c.WithLifetime(ContainerLifetime.Persistent)); + .RunAsContainer(c => c.WithPersistentLifetime()); privateEndpointsSubnet.AddPrivateEndpoint(sqlServer); var db = sqlServer.AddDatabase("sqldb"); diff --git a/playground/Stress/Stress.AppHost/AppHost.cs b/playground/Stress/Stress.AppHost/AppHost.cs index 7b28dc312ac..63f90f517e9 100644 --- a/playground/Stress/Stress.AppHost/AppHost.cs +++ b/playground/Stress/Stress.AppHost/AppHost.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; #pragma warning disable ASPIREDOTNETTOOL +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. var builder = DistributedApplication.CreateBuilder(args); builder.Services.AddHttpClient(); @@ -99,6 +100,9 @@ builder.AddExecutable("executableWithSingleArg", "dotnet", Environment.CurrentDirectory, "--version"); builder.AddExecutable("executableWithSingleEscapedArg", "dotnet", Environment.CurrentDirectory, "one two"); builder.AddExecutable("executableWithMultipleArgs", "dotnet", Environment.CurrentDirectory, "--version", "one two"); +var stressEmptyProjectPath = new Projects.Stress_Empty().ProjectPath; +builder.AddExecutable("persistentExecutable", "dotnet", Environment.CurrentDirectory, "run", "--project", stressEmptyProjectPath, "--no-build") + .WithPersistentLifetime(); IResourceBuilder? previousResourceBuilder = null; diff --git a/playground/TestShop/TestShop.AppHost/AppHost.cs b/playground/TestShop/TestShop.AppHost/AppHost.cs index a55a391f4ea..6ff51cbae4f 100644 --- a/playground/TestShop/TestShop.AppHost/AppHost.cs +++ b/playground/TestShop/TestShop.AppHost/AppHost.cs @@ -2,6 +2,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + var builder = DistributedApplication.CreateBuilder(args); var catalogDb = builder.AddPostgres("postgres") @@ -74,7 +76,7 @@ var messaging = builder.AddRabbitMQ("messaging") .WithDataVolume() - .WithLifetime(ContainerLifetime.Persistent) + .WithPersistentLifetime() .WithManagementPlugin() .PublishAsContainer(); diff --git a/playground/TypeScriptAppHost/apphost.ts b/playground/TypeScriptAppHost/apphost.ts index 38f24d7f6f9..fe03d4b03d1 100644 --- a/playground/TypeScriptAppHost/apphost.ts +++ b/playground/TypeScriptAppHost/apphost.ts @@ -8,7 +8,6 @@ import { createBuilder, refExpr, EnvironmentCallbackContext, - ContainerLifetime, ExecuteCommandContext, InputsDialogValidationContext, InputType @@ -50,7 +49,7 @@ console.log("Added Express API with reference to PostgreSQL database"); // Redis const cache = await builder.addRedis("cache"); -await cache.withLifetime(ContainerLifetime.Persistent); +await cache.withPersistentLifetime(); await cache.withCommand( "set-prefix", "Set prefix", diff --git a/playground/TypeScriptApps/RpsArena/apphost.ts b/playground/TypeScriptApps/RpsArena/apphost.ts index b0eb4a5695d..c01ae0cb9e6 100644 --- a/playground/TypeScriptApps/RpsArena/apphost.ts +++ b/playground/TypeScriptApps/RpsArena/apphost.ts @@ -1,7 +1,7 @@ // Rock Paper Scissors Arena — Aspire TypeScript AppHost // A polyglot game: C# Game Master, Python & Node.js players, React frontend, PostgreSQL -import { createBuilder, ContainerLifetime, type ExecuteCommandContext, type ExecuteCommandResult } from './.modules/aspire.js'; +import { createBuilder, type ExecuteCommandContext, type ExecuteCommandResult } from './.modules/aspire.js'; const builder = await createBuilder(); @@ -10,7 +10,7 @@ const builder = await createBuilder(); // Persistent lifetime so data survives restarts during development. const postgres = await builder .addPostgres("postgres") - .withLifetime(ContainerLifetime.Persistent) + .withPersistentLifetime() .withPgAdmin() .withDataVolume(); diff --git a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs index 124c88b511c..c4b5bc963c0 100644 --- a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs +++ b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. using System.Text; using System.Text.Json; @@ -273,22 +274,17 @@ public static IResourceBuilder RunAsEmulator(this IResou .AddAzureStorage($"{builder.Resource.Name}-storage") .WithParentRelationship(builder); - var lifetime = ContainerLifetime.Session; - - // Copy the lifetime from the main resource to the storage resource var surrogate = new AzureEventHubsEmulatorResource(builder.Resource); var surrogateBuilder = builder.ApplicationBuilder.CreateResourceBuilder(surrogate); if (configureContainer != null) { configureContainer(surrogateBuilder); - - if (surrogate.TryGetLastAnnotation(out var lifetimeAnnotation)) - { - lifetime = lifetimeAnnotation.Lifetime; - } + storageResource = storageResource.RunAsEmulator(c => c.WithLifetimeOf(surrogateBuilder)); + } + else + { + storageResource = storageResource.RunAsEmulator(); } - - storageResource = storageResource.RunAsEmulator(c => c.WithLifetime(lifetime)); var storage = storageResource.Resource; diff --git a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs index 13f2d48f272..0042acbac2e 100644 --- a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs +++ b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. using System.Text; using System.Text.Json; @@ -433,23 +434,15 @@ public static IResourceBuilder RunAsEmulator(this IReso context.EnvironmentVariables["MSSQL_SA_PASSWORD"] = passwordParameter; })); - var lifetime = ContainerLifetime.Session; - var surrogate = new AzureServiceBusEmulatorResource(builder.Resource); var surrogateBuilder = builder.ApplicationBuilder.CreateResourceBuilder(surrogate); if (configureContainer != null) { configureContainer(surrogateBuilder); - - if (surrogate.TryGetLastAnnotation(out var lifetimeAnnotation)) - { - lifetime = lifetimeAnnotation.Lifetime; - } + sqlServerResource = sqlServerResource.WithLifetimeOf(surrogateBuilder); } - sqlServerResource = sqlServerResource.WithLifetime(lifetime); - // RunAsEmulator() can be followed by custom model configuration so we need to delay the creation of the Config.json file // until all resources are about to be prepared and annotations can't be updated anymore. surrogateBuilder.WithContainerFiles( diff --git a/src/Aspire.Hosting.DevTunnels/DevTunnelHealthCheck.cs b/src/Aspire.Hosting.DevTunnels/DevTunnelHealthCheck.cs index baf42835038..f5fec38c837 100644 --- a/src/Aspire.Hosting.DevTunnels/DevTunnelHealthCheck.cs +++ b/src/Aspire.Hosting.DevTunnels/DevTunnelHealthCheck.cs @@ -34,11 +34,12 @@ public async Task CheckHealthAsync(HealthCheckContext context // Check that expected ports are active foreach (var portResource in _tunnelResource.Ports) { - var portStatus = tunnelStatus.Ports?.FirstOrDefault(p => p.PortNumber == portResource.TargetEndpoint.Port); + var tunnelPort = await portResource.GetTunnelPortAsync(cancellationToken).ConfigureAwait(false); + var portStatus = tunnelStatus.Ports?.FirstOrDefault(p => p.PortNumber == tunnelPort); portResource.LastKnownStatus = portStatus; if (portStatus?.PortUri is null) { - return HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelUnhealthy_PortInactive, _tunnelResource.TunnelId, portResource.TargetEndpoint.Port)); + return HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelUnhealthy_PortInactive, _tunnelResource.TunnelId, tunnelPort)); } } @@ -49,7 +50,8 @@ public async Task CheckHealthAsync(HealthCheckContext context // Get access status for each port foreach (var portResource in _tunnelResource.Ports) { - var portAccessStatus = await _devTunnelClient.GetAccessAsync(_tunnelResource.TunnelId, portResource.TargetEndpoint.Port, logger, cancellationToken).ConfigureAwait(false); + var tunnelPort = await portResource.GetTunnelPortAsync(cancellationToken).ConfigureAwait(false); + var portAccessStatus = await _devTunnelClient.GetAccessAsync(_tunnelResource.TunnelId, tunnelPort, logger, cancellationToken).ConfigureAwait(false); portResource.LastKnownAccessStatus = portAccessStatus; } diff --git a/src/Aspire.Hosting.DevTunnels/DevTunnelPortHealthCheck.cs b/src/Aspire.Hosting.DevTunnels/DevTunnelPortHealthCheck.cs index 4063d899b9a..025a6cf9c8e 100644 --- a/src/Aspire.Hosting.DevTunnels/DevTunnelPortHealthCheck.cs +++ b/src/Aspire.Hosting.DevTunnels/DevTunnelPortHealthCheck.cs @@ -6,40 +6,42 @@ namespace Aspire.Hosting.DevTunnels; -internal sealed class DevTunnelPortHealthCheck(DevTunnelResource tunnelResource, int port) : IHealthCheck +internal sealed class DevTunnelPortHealthCheck(DevTunnelPortResource portResource) : IHealthCheck { - private readonly DevTunnelResource _tunnelResource = tunnelResource ?? throw new ArgumentNullException(nameof(tunnelResource)); + private readonly DevTunnelPortResource _portResource = portResource ?? throw new ArgumentNullException(nameof(portResource)); - private readonly int _port = port; - - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { try { + var tunnelResource = _portResource.DevTunnel; + var port = await _portResource.GetTunnelPortAsync(cancellationToken).ConfigureAwait(false); var tunnelStatus = tunnelResource.LastKnownStatus; if (tunnelStatus is null) { - return Task.FromResult(HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelPortUnhealthy_StatusUnknown, _port, _tunnelResource.TunnelId))); + return HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelPortUnhealthy_StatusUnknown, port, tunnelResource.TunnelId)); } if (tunnelStatus.HostConnections == 0) { - return Task.FromResult(HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelUnhealthy_NoActiveHostConnections, _tunnelResource.TunnelId))); + return HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelUnhealthy_NoActiveHostConnections, tunnelResource.TunnelId)); } - var portStatus = tunnelStatus.Ports?.FirstOrDefault(p => p.PortNumber == _port); + var portStatus = tunnelStatus.Ports?.FirstOrDefault(p => p.PortNumber == port); // Check that port is active if (portStatus?.PortUri is null) { - return Task.FromResult(HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelUnhealthy_PortInactive, _tunnelResource.TunnelId, _port))); + return HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelUnhealthy_PortInactive, tunnelResource.TunnelId, port)); } - return Task.FromResult(HealthCheckResult.Healthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelPortHealthy, _port, _tunnelResource.TunnelId))); + return HealthCheckResult.Healthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelPortHealthy, port, tunnelResource.TunnelId)); } catch (Exception ex) { - return Task.FromResult(HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelPortUnhealthy_Error, _port, _tunnelResource.TunnelId, ex.Message), ex)); + var tunnelResource = _portResource.DevTunnel; + var port = _portResource.TargetEndpoint.TargetPort?.ToString(CultureInfo.InvariantCulture) ?? "unknown"; + return HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelPortUnhealthy_Error, port, tunnelResource.TunnelId, ex.Message), ex); } } -} \ No newline at end of file +} diff --git a/src/Aspire.Hosting.DevTunnels/DevTunnelResource.cs b/src/Aspire.Hosting.DevTunnels/DevTunnelResource.cs index b83b95a789e..ce72eaa4e24 100644 --- a/src/Aspire.Hosting.DevTunnels/DevTunnelResource.cs +++ b/src/Aspire.Hosting.DevTunnels/DevTunnelResource.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Globalization; using Aspire.Hosting.ApplicationModel; namespace Aspire.Hosting.DevTunnels; @@ -85,4 +86,43 @@ public DevTunnelPortResource( internal EndpointReference TargetEndpoint { get; init; } internal DevTunnelPort? LastKnownStatus { get; set; } internal DevTunnelAccessStatus? LastKnownAccessStatus { get; set; } + + internal async ValueTask GetTunnelPortAsync(CancellationToken cancellationToken = default) + { + if (TargetEndpoint.Resource.IsContainer()) + { + // Dev tunnel hosting runs on the host, so container endpoints must forward the host-reachable + // allocated port rather than the container-internal target port. + return await GetResolvedEndpointPortAsync(EndpointProperty.Port, cancellationToken).ConfigureAwait(false) + ?? TargetEndpoint.Port; + } + + if (TargetEndpoint.TargetPort is int targetPort) + { + return targetPort; + } + + return await GetResolvedEndpointPortAsync(EndpointProperty.TargetPort, cancellationToken).ConfigureAwait(false) + ?? TargetEndpoint.Port; + } + + private async ValueTask GetResolvedEndpointPortAsync(EndpointProperty property, CancellationToken cancellationToken) + { + string? resolvedTargetPort = null; + try + { + resolvedTargetPort = await TargetEndpoint.Property(property).GetValueAsync(cancellationToken).ConfigureAwait(false); + } + catch (InvalidOperationException) when (property == EndpointProperty.TargetPort && TargetEndpoint.IsAllocated) + { + // Endpoint references can only resolve targetPort dynamically when DCP reports a target-port expression. + } + + if (int.TryParse(resolvedTargetPort, NumberStyles.None, CultureInfo.InvariantCulture, out var port)) + { + return port; + } + + return null; + } } diff --git a/src/Aspire.Hosting.DevTunnels/DevTunnelResourceBuilderExtensions.cs b/src/Aspire.Hosting.DevTunnels/DevTunnelResourceBuilderExtensions.cs index c84801f8380..6c321bf48c1 100644 --- a/src/Aspire.Hosting.DevTunnels/DevTunnelResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.DevTunnels/DevTunnelResourceBuilderExtensions.cs @@ -177,7 +177,7 @@ public static IResourceBuilder AddDevTunnel( async Task DeleteUnmodeledPortsAsync() { var existingPorts = await devTunnelClient.GetPortListAsync(tunnelResource.TunnelId, logger, ct).ConfigureAwait(false); - var modeledPortNumbers = tunnelResource.Ports.Select(p => p.TargetEndpoint.Port).ToHashSet(); + var modeledPortNumbers = (await Task.WhenAll(tunnelResource.Ports.Select(p => p.GetTunnelPortAsync(ct).AsTask())).ConfigureAwait(false)).ToHashSet(); var unmodeledPorts = existingPorts.Ports.Where(p => !modeledPortNumbers.Contains(p.PortNumber)).ToList(); if (unmodeledPorts.Count > 0) { @@ -189,6 +189,7 @@ async Task DeleteUnmodeledPortsAsync() async Task StartPortAsync(DevTunnelPortResource portResource) { var portLogger = e.Services.GetRequiredService().GetLogger(portResource); + var tunnelPort = await portResource.GetTunnelPortAsync(ct).ConfigureAwait(false); // Clear any prior port status portLogger.LogInformation("Tunnel starting"); @@ -202,17 +203,17 @@ await notifications.PublishUpdateAsync(portResource, snapshot => snapshot with { _ = await devTunnelClient.CreatePortAsync( portResource.DevTunnel.TunnelId, - portResource.TargetEndpoint.Port, + tunnelPort, portResource.Options, portLogger, ct) .ConfigureAwait(false); - portLogger.LogInformation("Created dev tunnel port '{Port}' on tunnel '{Tunnel}' targeting endpoint '{Endpoint}' on resource '{TargetResource}'", portResource.TargetEndpoint.Port, portResource.DevTunnel.TunnelId, portResource.TargetEndpoint.EndpointName, portResource.TargetEndpoint.Resource.Name); + portLogger.LogInformation("Created dev tunnel port '{Port}' on tunnel '{Tunnel}' targeting endpoint '{Endpoint}' on resource '{TargetResource}'", tunnelPort, portResource.DevTunnel.TunnelId, portResource.TargetEndpoint.EndpointName, portResource.TargetEndpoint.Resource.Name); } catch (Exception ex) { - portLogger.LogError(ex, "Error trying to create dev tunnel port '{Port}' on tunnel '{Tunnel}': {Error}", portResource.TargetEndpoint.Port, portResource.DevTunnel.TunnelId, ex.Message); + portLogger.LogError(ex, "Error trying to create dev tunnel port '{Port}' on tunnel '{Tunnel}': {Error}", tunnelPort, portResource.DevTunnel.TunnelId, ex.Message); #pragma warning disable CS0618 // Type or member is obsolete portResource.TunnelEndpointAnnotation.AllocatedEndpointSnapshot.SetException(ex); #pragma warning restore CS0618 // Type or member is obsolete @@ -589,7 +590,7 @@ private static void AddDevTunnelPort( var healtCheckKey = $"{portName}-check"; tunnelBuilder.ApplicationBuilder.Services.AddHealthChecks().Add(new HealthCheckRegistration( healtCheckKey, - services => new DevTunnelPortHealthCheck(tunnel, targetEndpoint.Port), + services => new DevTunnelPortHealthCheck(portResource), failureStatus: default, tags: default, timeout: default)); diff --git a/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentBuilderExtension.cs b/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentBuilderExtension.cs index 8d9f7b1308f..18048c6fe64 100644 --- a/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentBuilderExtension.cs +++ b/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentBuilderExtension.cs @@ -111,7 +111,7 @@ public static IResourceBuilder PublishAsHostedAgent( var targetPort = existingHttpEndpoint?.TargetPort ?? 8088; builder - .WithHttpEndpoint(name: "http", env: "DEFAULT_AD_PORT", targetPort: targetPort) + .WithHttpEndpoint(name: "http", env: "DEFAULT_AD_PORT", targetPort: targetPort, isProxied: true) .WithUrls((ctx) => { var http = ctx.Urls.FirstOrDefault(u => u.Endpoint?.EndpointName == "http" || u.Endpoint?.EndpointName == "https"); diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs index e5afeaa80f5..3d1cbc9db7c 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs @@ -23,6 +23,8 @@ public sealed class EndpointAnnotation : IResourceAnnotation private int? _targetPort; private bool _targetPortSetToNull; private bool? _tlsEnabled; + private bool _isProxied = true; + private bool? _isExplicitlyProxied; private readonly NetworkIdentifier _networkID; /// @@ -35,7 +37,7 @@ public sealed class EndpointAnnotation : IResourceAnnotation /// Desired port for the service. /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. /// Indicates that this endpoint should be exposed externally at publish time. - /// Specifies if the endpoint will be proxied by DCP. Defaults to true. + /// Specifies if the endpoint will be proxied by DCP. Defaults to . public EndpointAnnotation( ProtocolType protocol, string? uriScheme = null, @@ -44,7 +46,7 @@ public EndpointAnnotation( int? port = null, int? targetPort = null, bool? isExternal = null, - bool isProxied = true + bool? isProxied = null ) : this( protocol, null, @@ -62,15 +64,48 @@ public EndpointAnnotation( /// Initializes a new instance of . /// /// Network protocol: TCP or UDP are supported today, others possibly in future. - /// The ID of the network that is the "default" network for the Endpoint. /// If a service is URI-addressable, this is the URI scheme to use for constructing service URI. /// Transport that is being used (e.g. http, http2, http3 etc). /// Name of the service. /// Desired port for the service. /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. /// Indicates that this endpoint should be exposed externally at publish time. - /// Specifies if the endpoint will be proxied by DCP. Defaults to true. + /// Specifies if the endpoint will be proxied by DCP. + public EndpointAnnotation( + ProtocolType protocol, + string? uriScheme, + string? transport, + [EndpointName] string? name, + int? port, + int? targetPort, + bool? isExternal, + bool isProxied + ) : this( + protocol, + null, + uriScheme, + transport, + name, + port, + targetPort, + isExternal, + isProxied + ) + { } + + /// + /// Initializes a new instance of . + /// + /// Network protocol: TCP or UDP are supported today, others possibly in future. + /// The ID of the network that is the "default" network for the Endpoint. /// Clients connected to the same network can reach the endpoint without any routing or network address translation. + /// If a service is URI-addressable, this is the URI scheme to use for constructing service URI. + /// Transport that is being used (e.g. http, http2, http3 etc). + /// Name of the service. + /// Desired port for the service. + /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. + /// Indicates that this endpoint should be exposed externally at publish time. + /// Specifies if the endpoint will be proxied by DCP. Defaults to . public EndpointAnnotation( ProtocolType protocol, NetworkIdentifier? networkID, @@ -80,7 +115,7 @@ public EndpointAnnotation( int? port = null, int? targetPort = null, bool? isExternal = null, - bool isProxied = true + bool? isProxied = null ) { // If the URI scheme is null, we'll adopt either udp:// or tcp:// based on the @@ -100,13 +135,49 @@ public EndpointAnnotation( _port = port; _targetPort = targetPort; IsExternal = isExternal ?? false; - IsProxied = isProxied; + IsExplicitlyProxied = isProxied; _networkID = networkID ?? KnownNetworkIdentifiers.LocalhostNetwork; #pragma warning disable CS0618 // Type or member is obsolete AllAllocatedEndpoints.TryAdd(_networkID, AllocatedEndpointSnapshot); #pragma warning restore CS0618 // Type or member is obsolete } + /// + /// Initializes a new instance of . + /// + /// Network protocol: TCP or UDP are supported today, others possibly in future. + /// The ID of the network that is the "default" network for the Endpoint. + /// Clients connected to the same network can reach the endpoint without any routing or network address translation. + /// If a service is URI-addressable, this is the URI scheme to use for constructing service URI. + /// Transport that is being used (e.g. http, http2, http3 etc). + /// Name of the service. + /// Desired port for the service. + /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. + /// Indicates that this endpoint should be exposed externally at publish time. + /// Specifies if the endpoint will be proxied by DCP. + public EndpointAnnotation( + ProtocolType protocol, + NetworkIdentifier? networkID, + string? uriScheme, + string? transport, + [EndpointName] string? name, + int? port, + int? targetPort, + bool? isExternal, + bool isProxied + ) : this( + protocol, + networkID, + uriScheme, + transport, + name, + port, + targetPort, + isExternal, + (bool?)isProxied + ) + { } + /// /// Name of the service /// @@ -137,6 +208,8 @@ public int? Port } } + internal int? SpecifiedPort => _port; + /// /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. /// @@ -180,10 +253,43 @@ public string Transport /// /// Indicates that this endpoint should be managed by DCP. This means it can be replicated and use a different port internally than the one publicly exposed. - /// Setting to false means the endpoint will be handled and exposed by the resource. + /// Setting to means the endpoint will be handled and exposed by the resource. /// - /// Defaults to true. - public bool IsProxied { get; set; } = true; + /// + /// Defaults to until DCP resolves the effective value from + /// and the resource that owns the endpoint. Read this value after endpoint allocation or another late lifecycle + /// callback when code needs the resolved value. + /// + public bool IsProxied + { + get => _isProxied; + set + { + _isProxied = value; + _isExplicitlyProxied = value; + } + } + + /// + /// Gets or sets a value indicating whether this endpoint was explicitly configured to be proxied. + /// + /// + /// A value means the effective proxy mode is resolved from the resource that owns the endpoint. + /// + public bool? IsExplicitlyProxied + { + get => _isExplicitlyProxied; + set + { + _isExplicitlyProxied = value; + _isProxied = value ?? true; + } + } + + internal void SetResolvedIsProxied(bool isProxied) + { + _isProxied = isProxied; + } /// /// Gets or sets a value indicating whether this endpoint is excluded from the default set when referencing the resource's endpoints diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs index 132d09340fa..343ae86cb65 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs @@ -229,6 +229,7 @@ public ReferenceExpression GetTlsValue(ReferenceExpression enabledValue, Referen _endpointAnnotation ??= Resource.Annotations.OfType() .SingleOrDefault(a => string.Equals(a.Name, EndpointName, StringComparisons.EndpointAnnotationName)); + return _endpointAnnotation; } diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointUpdateContext.cs b/src/Aspire.Hosting/ApplicationModel/EndpointUpdateContext.cs index a5c28ee495e..9f5e4ef779b 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointUpdateContext.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointUpdateContext.cs @@ -84,10 +84,10 @@ public bool IsExternal /// /// Gets or sets a value indicating whether the endpoint is proxied. /// - public bool IsProxied + public bool? IsProxied { - get => _endpointAnnotation.IsProxied; - set => _endpointAnnotation.IsProxied = value; + get => _endpointAnnotation.IsExplicitlyProxied; + set => _endpointAnnotation.IsExplicitlyProxied = value; } /// diff --git a/src/Aspire.Hosting/ApplicationModel/Lifetime.cs b/src/Aspire.Hosting/ApplicationModel/Lifetime.cs new file mode 100644 index 00000000000..851ab02a2bc --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/Lifetime.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Lifetime modes for resources that can outlive the app host process. +/// +public enum Lifetime +{ + /// + /// Create the resource when the app host process starts and dispose of it when the app host process shuts down. + /// + Session, + + /// + /// Attempt to re-use a previously created resource if one exists. Do not destroy the resource on app host process shutdown. + /// + Persistent, +} diff --git a/src/Aspire.Hosting/ApplicationModel/PersistenceAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/PersistenceAnnotation.cs new file mode 100644 index 00000000000..a4bd96396ad --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/PersistenceAnnotation.cs @@ -0,0 +1,62 @@ +// 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; +using System.Diagnostics.CodeAnalysis; + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Persistence modes for resources that support lifetime configuration. +/// +[Experimental("ASPIREPERSISTENCE001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] +public enum PersistenceMode +{ + /// + /// Create the resource when the app host process starts and dispose of it when the app host process shuts down. + /// + Session, + + /// + /// Attempt to re-use a previously created resource if one exists. Do not destroy the resource on app host process shutdown. + /// + Persistent, + + /// + /// Match another resource's persistence behavior. + /// + Resource, + + /// + /// Use persistent behavior scoped to a parent process identity. + /// + ParentProcess, +} + +/// +/// Annotation that controls the persistence behavior of a resource. +/// +[Experimental("ASPIREPERSISTENCE001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] +[DebuggerDisplay("Type = {GetType().Name,nq}, Mode = {Mode}")] +public sealed class PersistenceAnnotation : IResourceAnnotation +{ + /// + /// Gets or sets the persistence mode. + /// + public required PersistenceMode Mode { get; set; } + + /// + /// Gets or sets the source resource when is . + /// + public IResource? SourceResource { get; set; } + + /// + /// Gets or sets the parent process ID when is . + /// + public int? ParentProcessId { get; set; } + + /// + /// Gets or sets the parent process identity timestamp when is . + /// + public DateTime? ParentProcessTimestamp { get; set; } +} diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index 66fc4781786..d6aeecef5ee 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Persistence annotation APIs are experimental. + using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Aspire.Dashboard.Model; @@ -27,7 +29,9 @@ public static class ResourceExtensions [AspireExportIgnore(Reason = "Generic annotation inspection helper — not part of the ATS surface.")] public static bool TryGetLastAnnotation(this IResource resource, [NotNullWhen(true)] out T? annotation) where T : IResourceAnnotation { - if (resource.Annotations.OfType().LastOrDefault() is { } lastAnnotation) + var lastAnnotation = resource.Annotations.OfType().LastOrDefault(); + + if (lastAnnotation is not null) { annotation = lastAnnotation; return true; @@ -49,11 +53,11 @@ public static bool TryGetLastAnnotation(this IResource resource, [NotNullWhen [AspireExportIgnore(Reason = "Generic annotation inspection helper — not part of the ATS surface.")] public static bool TryGetAnnotationsOfType(this IResource resource, [NotNullWhen(true)] out IEnumerable? result) where T : IResourceAnnotation { - var matchingTypeAnnotations = resource.Annotations.OfType(); + var matchingTypeAnnotations = resource.Annotations.OfType().ToArray(); - if (matchingTypeAnnotations.Any()) + if (matchingTypeAnnotations.Length > 0) { - result = matchingTypeAnnotations.ToArray(); + result = matchingTypeAnnotations; return true; } else @@ -1039,22 +1043,111 @@ internal static bool IsBuildOnlyContainer(this IResource resource) } /// - /// Gets the lifetime type of the container for the specified resource. - /// Defaults to if no is found. + /// Gets the lifetime type for the specified resource. + /// Defaults to if no lifetime annotation is found. /// - /// The resource to get the ContainerLifetimeType for. + /// The resource to get the lifetime type for. /// - /// The from the for the resource (if the annotation exists). - /// Defaults to if the annotation is not set. + /// The from the for the resource (if the annotation exists). + /// Defaults to if the annotation is not set. /// - internal static ContainerLifetime GetContainerLifetimeType(this IResource resource) + internal static Lifetime GetLifetimeType(this IResource resource) + { + return GetLifetimeType(resource, []); + } + + private static Lifetime GetLifetimeType(IResource resource, HashSet visitedResources) { - if (resource.TryGetLastAnnotation(out var lifetimeAnnotation)) + if (!visitedResources.Add(resource)) { - return lifetimeAnnotation.Lifetime; + throw new InvalidOperationException($"A circular lifetime reference was detected for resource '{resource.Name}'."); } - return ContainerLifetime.Session; + if (resource.TryGetLastAnnotation(out var persistenceAnnotation)) + { + return persistenceAnnotation.Mode switch + { + PersistenceMode.Session => Lifetime.Session, + PersistenceMode.Persistent => Lifetime.Persistent, + PersistenceMode.Resource => persistenceAnnotation.SourceResource is { } sourceResource + ? GetLifetimeType(sourceResource, visitedResources) + : throw new InvalidOperationException($"Resource '{resource.Name}' has a resource persistence mode but no source resource."), + PersistenceMode.ParentProcess => Lifetime.Persistent, + _ => throw new InvalidOperationException($"Unknown persistence mode '{Enum.GetName(typeof(PersistenceMode), persistenceAnnotation.Mode)}'.") + }; + } + + if (resource.TryGetLastAnnotation(out var containerLifetimeAnnotation)) + { + return containerLifetimeAnnotation.Lifetime switch + { + ContainerLifetime.Session => Lifetime.Session, + ContainerLifetime.Persistent => Lifetime.Persistent, + _ => throw new InvalidOperationException($"Unknown container lifetime '{Enum.GetName(typeof(ContainerLifetime), containerLifetimeAnnotation.Lifetime)}'.") + }; + } + + return Lifetime.Session; + } + + /// + /// Determines whether the specified resource has a persistent lifetime. + /// + /// The resource to get persistent lifetime behavior for. + /// if the resource has a persistent container or executable lifetime, otherwise . + internal static bool HasPersistentLifetime(this IResource resource) + { + return resource.GetLifetimeType() == Lifetime.Persistent; + } + + internal static string GetOtelServiceInstanceId(this IResource resource, DcpInstance instance) + { + return resource.GetLifetimeType() == Lifetime.Persistent ? instance.Name : instance.Suffix; + } + + /// + /// Determines whether the specified resource has a parent process lifetime. + /// + /// The resource to get parent process lifetime behavior for. + /// The parent process ID if one exists. + /// The parent process identity timestamp if one exists. + /// if the resource has a parent process lifetime, otherwise . + internal static bool TryGetParentProcessLifetime(this IResource resource, out int parentProcessId, out DateTime parentProcessTimestamp) + { + return TryGetParentProcessLifetime(resource, [], out parentProcessId, out parentProcessTimestamp); + } + + private static bool TryGetParentProcessLifetime(IResource resource, HashSet visitedResources, out int parentProcessId, out DateTime parentProcessTimestamp) + { + if (!visitedResources.Add(resource)) + { + throw new InvalidOperationException($"A circular lifetime reference was detected for resource '{resource.Name}'."); + } + + if (resource.TryGetLastAnnotation(out var persistenceAnnotation)) + { + switch (persistenceAnnotation.Mode) + { + case PersistenceMode.ParentProcess when persistenceAnnotation.ParentProcessId is { } id && persistenceAnnotation.ParentProcessTimestamp is { } timestamp: + parentProcessId = id; + parentProcessTimestamp = timestamp; + return true; + case PersistenceMode.ParentProcess: + throw new InvalidOperationException($"Resource '{resource.Name}' has a parent process persistence mode but no parent process identity."); + case PersistenceMode.Resource: + return persistenceAnnotation.SourceResource is { } sourceResource + ? TryGetParentProcessLifetime(sourceResource, visitedResources, out parentProcessId, out parentProcessTimestamp) + : throw new InvalidOperationException($"Resource '{resource.Name}' has a resource persistence mode but no source resource."); + case PersistenceMode.Session or PersistenceMode.Persistent: + parentProcessId = 0; + parentProcessTimestamp = default; + return false; + } + } + + parentProcessId = 0; + parentProcessTimestamp = default; + return false; } /// @@ -1076,7 +1169,7 @@ internal static bool TryGetContainerImagePullPolicy(this IResource resource, [No } /// - /// Determines whether a resource has proxy support enabled or not. Container resources may have a setting that disables proxying for their + /// Determines whether a resource has proxy support enabled or not. Resources may have a setting that disables proxying for their /// endpoints regardless of the endpoint proxy configuration. /// /// The resource to get proxy support for. diff --git a/src/Aspire.Hosting/BuiltInDistributedApplicationEventSubscriptionHandlers.cs b/src/Aspire.Hosting/BuiltInDistributedApplicationEventSubscriptionHandlers.cs index f9cdb126112..7a2ece308aa 100644 --- a/src/Aspire.Hosting/BuiltInDistributedApplicationEventSubscriptionHandlers.cs +++ b/src/Aspire.Hosting/BuiltInDistributedApplicationEventSubscriptionHandlers.cs @@ -96,7 +96,7 @@ public static Task WarnPersistentContainersWithoutUserSecrets(BeforeStartEvent b foreach (var resource in beforeStartEvent.Model.Resources) { - if (resource.GetContainerLifetimeType() == ContainerLifetime.Persistent) + if (resource is ContainerResource && resource.GetLifetimeType() == Lifetime.Persistent) { if (logger.IsEnabled(LogLevel.Warning)) { diff --git a/src/Aspire.Hosting/CompatibilitySuppressions.xml b/src/Aspire.Hosting/CompatibilitySuppressions.xml index 1200e513ce8..8ecd3607985 100644 --- a/src/Aspire.Hosting/CompatibilitySuppressions.xml +++ b/src/Aspire.Hosting/CompatibilitySuppressions.xml @@ -29,4 +29,4 @@ lib/net8.0/Aspire.Hosting.dll true - \ No newline at end of file + diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index e6b294e3d60..d68b13ce307 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -4,6 +4,7 @@ #pragma warning disable ASPIREPIPELINES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning disable ASPIREPIPELINES003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only +#pragma warning disable ASPIREPERSISTENCE001 // Persistence annotation APIs are experimental. using System.Diagnostics.CodeAnalysis; using System.Text; @@ -22,6 +23,26 @@ namespace Aspire.Hosting; /// public static class ContainerResourceBuilderExtensions { + /// + /// Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + /// + /// The resource type. + /// The resource builder. + /// Should endpoints for the resource support using a proxy? + /// The . + /// + /// This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + /// port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + /// The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + /// endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + /// + // Keep this method on ContainerResourceBuilderExtensions for binary compatibility; moving it changes the declaring type in metadata. + [AspireExportIgnore(Reason = "Binary compatibility shim for the resource-level WithEndpointProxySupport overload.")] + public static IResourceBuilder WithEndpointProxySupport(this IResourceBuilder builder, bool proxyEnabled) where T : ContainerResource + { + return ResourceBuilderExtensions.SetEndpointProxySupport(builder, proxyEnabled); + } + /// /// Ensures that a container resource has PipelineStepAnnotations for building and pushing. /// @@ -532,16 +553,20 @@ public static IResourceBuilder WithContainerRuntimeArgs(this IResourceBuil /// /// The resource type. /// Builder for the container resource. - /// The lifetime behavior of the container resource. The defaults behavior is . + /// The lifetime behavior of the container resource. The default behavior is . /// The . /// + /// + /// Prefer or + /// for new code. + /// /// /// Marking a container resource to have a lifetime. /// /// var builder = DistributedApplication.CreateBuilder(args); /// /// builder.AddContainer("mycontainer", "myimage") - /// .WithLifetime(ContainerLifetime.Persistent); + /// .WithPersistentLifetime(); /// /// builder.Build().Run(); /// @@ -552,7 +577,17 @@ public static IResourceBuilder WithLifetime(this IResourceBuilder build { ArgumentNullException.ThrowIfNull(builder); - return builder.WithAnnotation(new ContainerLifetimeAnnotation { Lifetime = lifetime }, ResourceAnnotationMutationBehavior.Replace); + return builder + .WithAnnotation(new PersistenceAnnotation + { + Mode = lifetime switch + { + ContainerLifetime.Session => PersistenceMode.Session, + ContainerLifetime.Persistent => PersistenceMode.Persistent, + _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null) + } + }, ResourceAnnotationMutationBehavior.Replace) + .WithAnnotation(new ContainerLifetimeAnnotation { Lifetime = lifetime }, ResourceAnnotationMutationBehavior.Replace); } /// @@ -1462,30 +1497,6 @@ public static IResourceBuilder WithContainerFiles(this IResourceBuilder } } - /// - /// Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. - /// If set to false, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - /// - /// The type of container resource. - /// The resource builder for the container resource. - /// Should endpoints for the container resource support using a proxy? - /// The . - /// - /// This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - /// port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - /// The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - /// endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - /// - [AspireExport(Description = "Configures endpoint proxy support")] - public static IResourceBuilder WithEndpointProxySupport(this IResourceBuilder builder, bool proxyEnabled) where T : ContainerResource - { - ArgumentNullException.ThrowIfNull(builder); - - builder.WithAnnotation(new ProxySupportAnnotation { ProxyEnabled = proxyEnabled }, ResourceAnnotationMutationBehavior.Replace); - - return builder; - } - /// /// Builds the specified container image from a Dockerfile generated by a callback using the API. /// diff --git a/src/Aspire.Hosting/Dcp/ContainerCreator.cs b/src/Aspire.Hosting/Dcp/ContainerCreator.cs index 73474b4346a..19318f50d2c 100644 --- a/src/Aspire.Hosting/Dcp/ContainerCreator.cs +++ b/src/Aspire.Hosting/Dcp/ContainerCreator.cs @@ -112,7 +112,7 @@ internal void PrepareContainerNetworks() if (!containerResources.Any()) { return; } var network = ContainerNetwork.Create(KnownNetworkIdentifiers.DefaultAspireContainerNetwork.Value); - if (containerResources.Any(cr => cr.GetContainerLifetimeType() == ContainerLifetime.Persistent)) + if (containerResources.Any(cr => cr.GetLifetimeType() == Lifetime.Persistent)) { network.Spec.Persistent = true; network.Spec.NetworkName = $"{DcpExecutor.DefaultAspirePersistentNetworkName}-{_nameGenerator.GetProjectHashSuffix()}"; @@ -152,9 +152,10 @@ public IEnumerable> PrepareObjects() ctr.Spec.ContainerName = containerObjectInstance.Name; - if (container.GetContainerLifetimeType() == ContainerLifetime.Persistent) + if (container.GetLifetimeType() == Lifetime.Persistent) { ctr.Spec.Persistent = true; + ApplyMonitorProcess(container, ctr.Spec); } if (container.TryGetContainerImagePullPolicy(out var pullPolicy)) @@ -171,7 +172,7 @@ public IEnumerable> PrepareObjects() ctr.Annotate(CustomResource.ResourceNameAnnotation, container.Name); ctr.Annotate(CustomResource.OtelServiceNameAnnotation, container.Name); - ctr.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, containerObjectInstance.Suffix); + ctr.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, container.GetOtelServiceInstanceId(containerObjectInstance)); DcpExecutor.SetInitialResourceState(container, ctr); var aanns = container.Annotations.OfType().ToImmutableArray(); @@ -206,6 +207,15 @@ public IEnumerable> PrepareObjects() return result; } + private static void ApplyMonitorProcess(IResource resource, ContainerSpec spec) + { + if (resource.TryGetParentProcessLifetime(out var parentProcessId, out var parentProcessTimestamp)) + { + spec.MonitorPid = parentProcessId; + spec.MonitorTimestamp = parentProcessTimestamp; + } + } + private void ValidateContainerTunnelContainerNameConflicts(IEnumerable modelContainerResources) { if (!_options.Value.EnableAspireContainerTunnel) @@ -278,7 +288,7 @@ internal void PrepareContainerExecutables() workingDirectory: containerExecutable.WorkingDirectory); containerExec.Annotate(CustomResource.OtelServiceNameAnnotation, containerExecutable.Name); - containerExec.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, exeInstance.Suffix); + containerExec.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, containerExecutable.GetOtelServiceInstanceId(exeInstance)); containerExec.Annotate(CustomResource.ResourceNameAnnotation, containerExecutable.Name); DcpExecutor.SetInitialResourceState(containerExecutable, containerExec); @@ -941,9 +951,9 @@ private static List BuildContainerPorts(RenderedModelResource ContainerPort = ea.TargetPort, }; - if (!ea.IsProxied && ea.Port is int) + if (!ea.IsProxied && ea.SpecifiedPort is int hostPort) { - portSpec.HostPort = ea.Port; + portSpec.HostPort = hostPort; } switch (sp.EndpointAnnotation.Protocol) diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index cfdff4f0f3c..d56d7607b48 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -113,7 +113,7 @@ public DcpExecutor(ILogger logger, _executionContext = executionContext; _appResources = appResources; - _resourceWatcher = new DcpResourceWatcher(logger, kubernetesService, loggerService, executorEvents, model, _appResources, _configuration, profilingTelemetry, _shutdownCancellation.Token); + _resourceWatcher = new DcpResourceWatcher(logger, kubernetesService, loggerService, executorEvents, model, _appResources, _configuration, PublishLateEndpointsAllocatedEventAsync, profilingTelemetry, _shutdownCancellation.Token); DeleteResourceRetryPipeline = DcpPipelineBuilder.BuildDeleteRetryPipeline(logger); @@ -195,46 +195,65 @@ public async Task RunApplicationAsync(CancellationToken ct = default) var createContainerNetworks = Task.Run(() => CreateAllDcpObjectsAsync(ct), ct); - var createExecutableEndpoints = Task.Run(async () => + var createWorkloadEndpoints = Task.Run(async () => { - await getProxyAddresses.ConfigureAwait(false); + await Task.WhenAll([getProxyAddresses, createContainerNetworks]).WaitAsync(ct).ConfigureAwait(false); + + List endpointAllocatedResources = []; + foreach (var executable in executables) + { + if (DcpModelUtilities.TryAddWorkloadAllocatedEndpoints( + executable, + _options.Value.EnableAspireContainerTunnel, + ContainerHostName, + allowPendingDynamicProxylessContainerEndpoints: false)) + { + endpointAllocatedResources.Add(executable.ModelResource); + } + } - DcpModelUtilities.AddWorkloadAllocatedEndpoints( - executables, - _options.Value.EnableAspireContainerTunnel, - ContainerHostName); - await PublishEndpointsAllocatedEventAsync(executables, ct).ConfigureAwait(false); + foreach (var container in containers) + { + if (DcpModelUtilities.TryAddWorkloadAllocatedEndpoints( + container, + _options.Value.EnableAspireContainerTunnel, + ContainerHostName, + allowPendingDynamicProxylessContainerEndpoints: true)) + { + endpointAllocatedResources.Add(container.ModelResource); + } + } + + // Allocate every endpoint that is known before workload creation before publishing any + // resource-specific endpoint events. URL callbacks can reference endpoints on other + // resources, so publishing per-resource while another resource is still allocating can + // make a valid cross-resource callback observe an unallocated endpoint. + foreach (var resource in endpointAllocatedResources.Distinct()) + { + await PublishEndpointsAllocatedEventAsync(resource, ct).ConfigureAwait(false); + } }, ct); var createExecutables = Task.Run(async () => { - await createExecutableEndpoints.ConfigureAwait(false); + await createWorkloadEndpoints.ConfigureAwait(false); await CreateRenderedResourcesAsync(_executableCreator, executables, EmptyCreationContext.s_instance, ct).ConfigureAwait(false); }, ct); // Configuring containers that use the tunnel require these host network-side endpoints for Executables to be ready. - var cctx = new ContainerCreationContext(createContainerNetworks, createExecutableEndpoints, ct); + var cctx = new ContainerCreationContext(createContainerNetworks, createWorkloadEndpoints, ct); _containerContextSource.SetResult(cctx); var createContainers = Task.Run(async () => { - await Task.WhenAll([getProxyAddresses, createContainerNetworks]).WaitAsync(ct).ConfigureAwait(false); - - // Allocate container workload endpoints, then publish endpoint-allocated events. - DcpModelUtilities.AddWorkloadAllocatedEndpoints( - containers, - _options.Value.EnableAspireContainerTunnel, - ContainerHostName); - await PublishEndpointsAllocatedEventAsync(containers, ct).ConfigureAwait(false); + await createWorkloadEndpoints.ConfigureAwait(false); await CreateRenderedResourcesAsync(_containerCreator, containers, cctx, ct).ConfigureAwait(false); }, ct); // Now wait for all "leaf" creations to complete. await Task.WhenAll(createExecutables, createContainers).WaitAsync(ct).ConfigureAwait(false); - - await _executorEvents.PublishAsync(new OnEndpointsAllocatedContext(ct)).ConfigureAwait(false); } catch (Exception ex) { @@ -637,11 +656,7 @@ private void PrepareServices() var svc = Service.Create(serviceName); - if (!sp.ModelResource.SupportsProxy()) - { - // If the resource shouldn't be proxied, we need to enforce that on the annotation - endpoint.IsProxied = false; - } + endpoint.SetResolvedIsProxied(GetEffectiveIsProxied(sp.ModelResource, endpoint)); int? port; if (_options.Value.RandomizePorts && endpoint.IsProxied && endpoint.Port != null) @@ -651,7 +666,9 @@ private void PrepareServices() } else { - port = endpoint.Port; + port = sp.ModelResource.IsContainer() && !endpoint.IsProxied + ? endpoint.SpecifiedPort + : endpoint.Port; } svc.Spec.Port = port; svc.Spec.Protocol = PortProtocol.FromProtocolType(endpoint.Protocol); @@ -678,6 +695,21 @@ private void PrepareServices() } } + static bool GetEffectiveIsProxied(IResource resource, EndpointAnnotation endpoint) + { + if (!resource.SupportsProxy()) + { + return false; + } + + if (endpoint.IsExplicitlyProxied is bool isProxied) + { + return isProxied; + } + + return !resource.HasPersistentLifetime(); + } + var containers = _model.Resources.Where(r => r.IsContainer()); if (!containers.Any()) { @@ -799,7 +831,33 @@ await _executorEvents.PublishAsync(new OnResourceChangedContext( return; } - await _executorEvents.PublishAsync(new OnConnectionStringAvailableContext(cancellationToken, modelResource)).ConfigureAwait(false); + if (replicas.All(r => IsDelayedStart(r.DcpResource))) + { + // DCP resources with Spec.Start=false are created now so they are visible to DCP and the dashboard, + // but their process/container is not actually started until StartResourceAsync flips Spec.Start to true. + // Keep BeforeResourceStartedEvent tied to the actual start operation rather than object creation. + foreach (var r in replicas) + { + await _executorEvents.PublishAsync(new OnResourceChangedContext( + cancellationToken, resourceType, modelResource, + r.DcpResource.Metadata.Name, + new ResourceStatus(KnownResourceStates.NotStarted, null, null), + s => s with + { + State = new ResourceStateSnapshot(KnownResourceStates.NotStarted, null) + }) + ).ConfigureAwait(false); + } + + foreach (var er in replicas) + { + await CreateReplicaAsync(er).ConfigureAwait(false); + } + + return; + } + + await PublishConnectionStringAvailableEventAsync(modelResource, cancellationToken).ConfigureAwait(false); // For single-replica resources (e.g. containers), include the DCP resource name in the starting event. // For multi-replica resources (e.g. projects with replicas), the starting event applies to the group, so DcpResourceName is null. @@ -808,26 +866,7 @@ await _executorEvents.PublishAsync(new OnResourceChangedContext( foreach (var er in replicas) { - try - { - await creator.CreateObjectAsync(er, context, resourceLogger, this, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - throw; - } - catch (FailedToApplyEnvironmentException ex) - { - // For this exception we don't want the noise of the stack trace, we've already - // provided more detail where we detected the issue (e.g. envvar name). To get - // more diagnostic information reduce logging level for DCP log category to Debug. - await _executorEvents.PublishAsync(new OnResourceFailedToStartContext(cancellationToken, resourceType, er.ModelResource, er.DcpResource.Metadata.Name, ex.Message)).ConfigureAwait(false); - } - catch (Exception ex) - { - resourceLogger.LogError(ex, "Failed to create resource {ResourceName}", er.ModelResource.Name); - await _executorEvents.PublishAsync(new OnResourceFailedToStartContext(cancellationToken, resourceType, er.ModelResource, er.DcpResource.Metadata.Name)).ConfigureAwait(false); - } + await CreateReplicaAsync(er).ConfigureAwait(false); } } catch (Exception ex) @@ -854,6 +893,40 @@ Func BuildSnapshotFunc(CustomRes _ => throw new NotImplementedException($"Does not support snapshots for resources of type '{dcpResource.Kind}'") }; } + + async Task CreateReplicaAsync(RenderedModelResource er) + { + try + { + await creator.CreateObjectAsync(er, context, resourceLogger, this, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (FailedToApplyEnvironmentException ex) + { + // For this exception we don't want the noise of the stack trace, we've already + // provided more detail where we detected the issue (e.g. envvar name). To get + // more diagnostic information reduce logging level for DCP log category to Debug. + await _executorEvents.PublishAsync(new OnResourceFailedToStartContext(cancellationToken, resourceType, er.ModelResource, er.DcpResource.Metadata.Name, ex.Message)).ConfigureAwait(false); + } + catch (Exception ex) + { + resourceLogger.LogError(ex, "Failed to create resource {ResourceName}", er.ModelResource.Name); + await _executorEvents.PublishAsync(new OnResourceFailedToStartContext(cancellationToken, resourceType, er.ModelResource, er.DcpResource.Metadata.Name)).ConfigureAwait(false); + } + } + + static bool IsDelayedStart(CustomResource resource) + { + return resource switch + { + Container { Spec.Start: false } => true, + Executable { Spec.Start: false } => true, + _ => false + }; + } } /// @@ -1007,7 +1080,7 @@ public async Task StartResourceAsync(IResourceReference resourceReference, Cance // Ensure we explicitly start the container even if original container was created in "delay-start" mode. cr.DcpResource.Spec.Start = true; - await _executorEvents.PublishAsync(new OnConnectionStringAvailableContext(cancellationToken, resourceReference.ModelResource)).ConfigureAwait(false); + await PublishConnectionStringAvailableEventAsync(resourceReference.ModelResource, cancellationToken).ConfigureAwait(false); await _executorEvents.PublishAsync(new OnResourceStartingContext(cancellationToken, resourceType, resourceReference.ModelResource, resourceReference.DcpResourceName)).ConfigureAwait(false); var cctx = await _containerContextSource.Task.ConfigureAwait(false); await _containerCreator.CreateObjectAsync(cr, cctx, resourceLogger, this, cancellationToken).ConfigureAwait(false); @@ -1015,7 +1088,10 @@ public async Task StartResourceAsync(IResourceReference resourceReference, Cance case RenderedModelResource er: await EnsureResourceDeletedAsync(resourceReference.DcpResourceName, cancellationToken).ConfigureAwait(false); - await _executorEvents.PublishAsync(new OnConnectionStringAvailableContext(cancellationToken, resourceReference.ModelResource)).ConfigureAwait(false); + // Ensure we explicitly start the executable even if original executable was created in "delay-start" mode. + er.DcpResource.Spec.Start = true; + + await PublishConnectionStringAvailableEventAsync(resourceReference.ModelResource, cancellationToken).ConfigureAwait(false); await _executorEvents.PublishAsync(new OnResourceStartingContext(cancellationToken, resourceType, resourceReference.ModelResource, resourceReference.DcpResourceName)).ConfigureAwait(false); await _executableCreator.CreateObjectAsync(er, EmptyCreationContext.s_instance, resourceLogger, this, cancellationToken).ConfigureAwait(false); break; @@ -1116,21 +1192,36 @@ private static void ForgetCachedCallbackResults(IResource resource) } } - private async Task PublishEndpointsAllocatedEventAsync(IEnumerable> resource, CancellationToken ct) - where TDcpResource : CustomResource, IKubernetesStaticMetadata + private async Task PublishEndpointsAllocatedEventAsync(IResource resource, CancellationToken ct) { - foreach (var r in resource) + lock (_endpointsAdvertised) { - lock (_endpointsAdvertised) + if (!_endpointsAdvertised.Add(resource.Name)) { - if (!_endpointsAdvertised.Add(r.ModelResource.Name)) - { - continue; // Already published for this resource - } + return false; // Already published for this resource. } + } - var ev = new ResourceEndpointsAllocatedEvent(r.ModelResource, _executionContext.ServiceProvider); - await _distributedApplicationEventing.PublishAsync(ev, EventDispatchBehavior.NonBlockingConcurrent, ct).ConfigureAwait(false); + var ev = new ResourceEndpointsAllocatedEvent(resource, _executionContext.ServiceProvider); + await _distributedApplicationEventing.PublishAsync(ev, EventDispatchBehavior.BlockingSequential, ct).ConfigureAwait(false); + return true; + } + + private async Task PublishLateEndpointsAllocatedEventAsync(IResource resource, CancellationToken ct) + { + if (await PublishEndpointsAllocatedEventAsync(resource, ct).ConfigureAwait(false)) + { + await PublishConnectionStringAvailableEventAsync(resource, ct).ConfigureAwait(false); } } + + private async Task PublishConnectionStringAvailableEventAsync(IResource resource, CancellationToken ct) + { + if (!DcpModelUtilities.AreResourceEndpointsAllocated(resource)) + { + return; + } + + await _executorEvents.PublishAsync(new OnConnectionStringAvailableContext(ct, resource)).ConfigureAwait(false); + } } diff --git a/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs b/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs index 992a9da8b76..6801c2b6768 100644 --- a/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs +++ b/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs @@ -92,70 +92,177 @@ internal static void AddWorkloadAllocatedEndpoints( { foreach (var res in resources) { - foreach (var sp in res.ServicesProduced) + TryAddWorkloadAllocatedEndpoints(res, enableAspireContainerTunnel, containerHostName, allowPendingDynamicProxylessContainerEndpoints: false); + } + } + + internal static bool TryAddWorkloadAllocatedEndpoints( + RenderedModelResource resource, + bool enableAspireContainerTunnel, + string containerHostName, + bool allowPendingDynamicProxylessContainerEndpoints) + where TDcpResource : CustomResource, IKubernetesStaticMetadata + { + foreach (var sp in resource.ServicesProduced) + { + if (TryAddLocalhostAllocatedEndpoint( + sp, + allowPending: allowPendingDynamicProxylessContainerEndpoints && IsDynamicProxylessContainerEndpoint(resource, sp))) { - var svc = sp.DcpResource; + AddContainerNetworkAllocatedEndpoint(resource, sp); + AddExecutableContainerNetworkAllocatedEndpoint(resource, sp, enableAspireContainerTunnel, containerHostName); + } + } - if (!svc.HasCompleteAddress && sp.EndpointAnnotation.IsProxied) - { - // This should never happen; if it does, we have a bug without a workaround for the user. - // We should have waited for the service to have a complete address before getting here. - throw new InvalidDataException($"Service {svc.Metadata.Name} should have valid address at this point"); - } + return AreResourceEndpointsAllocated(resource.ModelResource); + } - if (!sp.EndpointAnnotation.IsProxied && svc.AllocatedPort is null) - { - throw new InvalidOperationException($"Service '{svc.Metadata.Name}' needs to specify a port for endpoint '{sp.EndpointAnnotation.Name}' since it isn't using a proxy."); - } + internal static bool TryApplyServiceAddressToEndpoint(Service observedService, IEnumerable appResources, [NotNullWhen(true)] out IResource? modelResource) + { + var serviceResource = appResources.OfType() + .FirstOrDefault(swr => string.Equals(swr.DcpResource.Metadata.Name, observedService.Metadata.Name, StringComparison.Ordinal)); + + if (serviceResource is null) + { + modelResource = null; + return false; + } - var (targetHost, bindingMode) = NormalizeTargetHost(sp.EndpointAnnotation.TargetHost); + serviceResource.Service.ApplyAddressInfoFrom(observedService); + var isDynamicProxylessContainerEndpoint = appResources.OfType>() + .Any(resource => ReferenceEquals(resource.ModelResource, serviceResource.ModelResource) && + IsDynamicProxylessContainerEndpoint(resource, serviceResource)); + if (!TryAddLocalhostAllocatedEndpoint(serviceResource, allowPending: true)) + { + modelResource = null; + return false; + } - sp.EndpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint( - sp.EndpointAnnotation, - targetHost, - (int)svc.AllocatedPort!, - bindingMode, - targetPortExpression: $$$"""{{- portForServing "{{{svc.Metadata.Name}}}" -}}""", - KnownNetworkIdentifiers.LocalhostNetwork); + foreach (var containerResource in appResources.OfType>() + .Where(resource => ReferenceEquals(resource.ModelResource, serviceResource.ModelResource))) + { + AddContainerNetworkAllocatedEndpoint(containerResource, serviceResource); + } - if (res.DcpResource is Container ctr && ctr.Spec.Networks is not null) - { - // Once container networks are fully supported, this should allocate endpoints on those networks - var containerNetwork = ctr.Spec.Networks.FirstOrDefault(n => n.Name == KnownNetworkIdentifiers.DefaultAspireContainerNetwork.Value); - - if (containerNetwork is not null) - { - var port = sp.EndpointAnnotation.TargetPort!; - - var allocatedEndpoint = new AllocatedEndpoint( - sp.EndpointAnnotation, - $"{sp.ModelResource.Name}.dev.internal", - (int)port, - EndpointBindingMode.SingleAddress, - targetPortExpression: $$$"""{{- portForServing "{{{svc.Metadata.Name}}}" -}}""", - KnownNetworkIdentifiers.DefaultAspireContainerNetwork - ); - sp.EndpointAnnotation.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(allocatedEndpoint.NetworkID, allocatedEndpoint); - } - } + modelResource = serviceResource.ModelResource; + return isDynamicProxylessContainerEndpoint && AreResourceEndpointsAllocated(modelResource); + } - // If we are not using the tunnel, we can project Executable endpoints into container networks via ContainerHostName. - // This really only works for Docker Desktop, but it is useful for testing too. - if (res.DcpResource is Executable && !enableAspireContainerTunnel) - { - var port = sp.EndpointAnnotation.TargetPort!; - var allocatedEndpoint = new AllocatedEndpoint( - sp.EndpointAnnotation, - containerHostName, - (int)svc.AllocatedPort!, - EndpointBindingMode.SingleAddress, - targetPortExpression: $$$"""{{- portForServing "{{{svc.Metadata.Name}}}" -}}""", - KnownNetworkIdentifiers.DefaultAspireContainerNetwork - ); - sp.EndpointAnnotation.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, allocatedEndpoint); - } + private static bool TryAddLocalhostAllocatedEndpoint(ServiceWithModelResource sp, bool allowPending) + { + var svc = sp.DcpResource; + + if (sp.EndpointAnnotation.AllocatedEndpoint is not null) + { + return true; + } + + if (!svc.HasCompleteAddress && sp.EndpointAnnotation.IsProxied) + { + if (allowPending) + { + return false; } + + // This should never happen; if it does, we have a bug without a workaround for the user. + // We should have waited for the service to have a complete address before getting here. + throw new InvalidDataException($"Service {svc.Metadata.Name} should have valid address at this point"); } + + if (!sp.EndpointAnnotation.IsProxied && svc.AllocatedPort is null) + { + if (allowPending) + { + return false; + } + + throw new InvalidOperationException($"Service '{svc.Metadata.Name}' needs to specify a port for endpoint '{sp.EndpointAnnotation.Name}' since it isn't using a proxy."); + } + + if (!svc.HasCompleteAddress) + { + if (allowPending) + { + return false; + } + + throw new InvalidDataException($"Service {svc.Metadata.Name} should have valid address at this point"); + } + + var (targetHost, bindingMode) = NormalizeTargetHost(sp.EndpointAnnotation.TargetHost); + + sp.EndpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint( + sp.EndpointAnnotation, + targetHost, + (int)svc.AllocatedPort!, + bindingMode, + targetPortExpression: $$$"""{{- portForServing "{{{svc.Metadata.Name}}}" -}}""", + KnownNetworkIdentifiers.LocalhostNetwork); + + return true; + } + + private static void AddContainerNetworkAllocatedEndpoint(RenderedModelResource resource, ServiceWithModelResource sp) + where TDcpResource : CustomResource, IKubernetesStaticMetadata + { + if (resource.DcpResource is not Container ctr || ctr.Spec.Networks is null) + { + return; + } + + // Once container networks are fully supported, this should allocate endpoints on those networks. + var containerNetwork = ctr.Spec.Networks.FirstOrDefault(n => n.Name == KnownNetworkIdentifiers.DefaultAspireContainerNetwork.Value); + + if (containerNetwork is null) + { + return; + } + + var port = sp.EndpointAnnotation.TargetPort!; + + var allocatedEndpoint = new AllocatedEndpoint( + sp.EndpointAnnotation, + $"{sp.ModelResource.Name}.dev.internal", + (int)port, + EndpointBindingMode.SingleAddress, + targetPortExpression: $$$"""{{- portForServing "{{{sp.DcpResource.Metadata.Name}}}" -}}""", + KnownNetworkIdentifiers.DefaultAspireContainerNetwork + ); + sp.EndpointAnnotation.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(allocatedEndpoint.NetworkID, allocatedEndpoint); + } + + private static void AddExecutableContainerNetworkAllocatedEndpoint(RenderedModelResource resource, ServiceWithModelResource sp, bool enableAspireContainerTunnel, string containerHostName) + where TDcpResource : CustomResource, IKubernetesStaticMetadata + { + if (resource.DcpResource is not Executable || enableAspireContainerTunnel) + { + return; + } + + // If we are not using the tunnel, we can project Executable endpoints into container networks via ContainerHostName. + // This really only works for Docker Desktop, but it is useful for testing too. + var allocatedEndpoint = new AllocatedEndpoint( + sp.EndpointAnnotation, + containerHostName, + (int)sp.DcpResource.AllocatedPort!, + EndpointBindingMode.SingleAddress, + targetPortExpression: $$$"""{{- portForServing "{{{sp.DcpResource.Metadata.Name}}}" -}}""", + KnownNetworkIdentifiers.DefaultAspireContainerNetwork + ); + sp.EndpointAnnotation.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, allocatedEndpoint); + } + + internal static bool AreResourceEndpointsAllocated(IResource resource) + { + return !resource.TryGetEndpoints(out var endpoints) || endpoints.All(e => e.AllocatedEndpoint is not null); + } + + private static bool IsDynamicProxylessContainerEndpoint(RenderedModelResource resource, ServiceWithModelResource sp) + where TDcpResource : CustomResource, IKubernetesStaticMetadata + { + return resource.DcpResource is Container && + !sp.EndpointAnnotation.IsProxied && + sp.EndpointAnnotation.SpecifiedPort is null; } internal static void AddContainerTunnelAllocatedEndpoints( diff --git a/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs b/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs index 742ce696b83..4ef706d4a62 100644 --- a/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs +++ b/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs @@ -14,7 +14,7 @@ internal sealed class DcpNameGenerator // A random suffix added to every DCP object name ensures that those names (and derived object names, for example container names) // are unique machine-wide with a high level of probability. // The length of 8 achieves that while keeping the names relatively short and readable. - // The second purpose of the suffix is to play a role of a unique OpenTelemetry service instance ID. + // The second purpose of the suffix is to play the role of a unique OpenTelemetry service instance ID for session resources. private const int RandomNameSuffixLength = 8; private readonly IConfiguration _configuration; private readonly IOptions _options; @@ -34,6 +34,8 @@ public DcpNameGenerator(IConfiguration configuration, IOptions optio public void EnsureDcpInstancesPopulated(IResource resource) { + ThrowIfPersistentExecutableHasReplicas(resource); + if (resource.TryGetInstances(out _)) { return; @@ -67,11 +69,24 @@ private static void AddInstancesAnnotation(IResource resource, ImmutableArray 1 && resource.GetLifetimeType() == Lifetime.Persistent) + { + throw new InvalidOperationException($"Resource '{resource.Name}' uses multiple replicas and a persistent lifetime. These features do not work together."); + } + } + public (string Name, string Suffix) GetContainerName(IResource container) { - var nameSuffix = container.GetContainerLifetimeType() switch + var nameSuffix = container.GetLifetimeType() switch { - ContainerLifetime.Session => GetRandomNameSuffix(), + Lifetime.Session => GetRandomNameSuffix(), _ => GetProjectHashSuffix(), }; @@ -80,7 +95,12 @@ private static void AddInstancesAnnotation(IResource resource, ImmutableArray GetRandomNameSuffix(), + _ => GetProjectHashSuffix(), + }; + return (GetObjectNameForResource(project, _options.Value, nameSuffix), nameSuffix); } diff --git a/src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs b/src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs new file mode 100644 index 00000000000..53a96fa7931 --- /dev/null +++ b/src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs @@ -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 System.Globalization; +using System.Runtime.InteropServices; +using SystemProcess = System.Diagnostics.Process; + +namespace Aspire.Hosting.Dcp; + +internal sealed record DcpProcessIdentity(int ProcessId, DateTime Timestamp); + +internal static partial class DcpProcessMonitor +{ + private const int DefaultLinuxClockTicksPerSecond = 100; + private const int LinuxClockTicksPerSecondConfigName = 2; // _SC_CLK_TCK + + internal static DcpProcessIdentity GetMonitorProcessIdentity(SystemProcess parentProcess) + { + ArgumentNullException.ThrowIfNull(parentProcess); + + var monitorProcessId = parentProcess.Id; + var timestamp = GetProcessIdentityTimestamp(parentProcess); + + if (timestamp is null) + { + throw new InvalidOperationException($"Could not determine the identity timestamp for monitor process {monitorProcessId}."); + } + + return new(monitorProcessId, timestamp.Value); + } + + private static DateTime? GetProcessIdentityTimestamp(SystemProcess parentProcess) + { + if (OperatingSystem.IsLinux()) + { + return GetLinuxProcessIdentityTimestamp(parentProcess.Id); + } + + try + { + return parentProcess.StartTime.ToUniversalTime(); + } + catch (ArgumentException) + { + return null; + } + catch (InvalidOperationException) + { + return null; + } + } + + private static DateTime? GetLinuxProcessIdentityTimestamp(int processId) + { + var statPath = Path.Combine( + Environment.GetEnvironmentVariable("HOST_PROC") ?? "/proc", + processId.ToString(CultureInfo.InvariantCulture), + "stat"); + + string contents; + try + { + contents = File.ReadAllText(statPath); + } + catch (IOException ex) + { + throw new InvalidOperationException($"Could not read monitor process stat file '{statPath}'.", ex); + } + catch (UnauthorizedAccessException ex) + { + throw new InvalidOperationException($"Could not read monitor process stat file '{statPath}'.", ex); + } + + // /proc//stat fields start as: + // 12345 (process name may contain spaces or parentheses) S 1 2 3 ... + // The process start time is field 22, in clock ticks since boot. Match DCP's + // Linux identity time by converting that monotonic value into a DateTime + // offset from DateTime.MinValue instead of estimating a wall-clock time. + var closeParenIndex = contents.LastIndexOf(')'); + if (closeParenIndex < 0 || closeParenIndex + 2 >= contents.Length) + { + throw new InvalidOperationException($"Monitor process stat file '{statPath}' was malformed."); + } + + var fields = contents[(closeParenIndex + 2)..].Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (fields.Length < 20 || !ulong.TryParse(fields[19], CultureInfo.InvariantCulture, out var startTicks)) + { + throw new InvalidOperationException($"Monitor process stat file '{statPath}' did not contain a valid start time."); + } + + var startTimeMilliseconds = (startTicks * 1000) / (ulong)GetLinuxClockTicksPerSecond(); + return DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc).AddMilliseconds(startTimeMilliseconds); + } + + private static int GetLinuxClockTicksPerSecond() + { + var result = sysconf(LinuxClockTicksPerSecondConfigName); + return result > 0 ? (int)result : DefaultLinuxClockTicksPerSecond; + } + + [LibraryImport("libc", SetLastError = true, EntryPoint = "sysconf")] + private static partial long sysconf(int name); +} diff --git a/src/Aspire.Hosting/Dcp/DcpResourceWatcher.cs b/src/Aspire.Hosting/Dcp/DcpResourceWatcher.cs index 9e5773f3a0d..876933b6932 100644 --- a/src/Aspire.Hosting/Dcp/DcpResourceWatcher.cs +++ b/src/Aspire.Hosting/Dcp/DcpResourceWatcher.cs @@ -30,6 +30,7 @@ internal sealed class DcpResourceWatcher : IConsoleLogsService, IAsyncDisposable private readonly DcpExecutorEvents _executorEvents; private readonly ILogger _logger; private readonly IConfiguration _configuration; + private readonly Func _publishEndpointsAllocatedEventAsync; private readonly ProfilingTelemetry _profilingTelemetry; private readonly CancellationToken _shutdownToken; @@ -56,6 +57,7 @@ public DcpResourceWatcher( DistributedApplicationModel model, DcpAppResourceStore appResources, IConfiguration configuration, + Func publishEndpointsAllocatedEventAsync, ProfilingTelemetry profilingTelemetry, CancellationToken shutdownToken) { @@ -64,6 +66,7 @@ public DcpResourceWatcher( _executorEvents = executorEvents; _logger = logger; _configuration = configuration; + _publishEndpointsAllocatedEventAsync = publishEndpointsAllocatedEventAsync; _profilingTelemetry = profilingTelemetry; _shutdownToken = shutdownToken; @@ -325,6 +328,12 @@ internal static ResourceStatus GetResourceStatus(CustomResource resource) } if (resource is Executable executable) { + if (executable.Spec.Start == false && IsNotStartedExecutableState(executable.Status?.State)) + { + // If the resource is set for delay start, treat not-yet-started states as NotStarted. + return new(KnownResourceStates.NotStarted, null, null); + } + return new(executable.Status?.State, executable.Status?.StartupTimestamp?.ToUniversalTime(), executable.Status?.FinishTimestamp?.ToUniversalTime()); } if (resource is ContainerExec containerExec) @@ -347,6 +356,11 @@ private void AddDcpResourceObservedEvent(CustomResource resource, IResource appM resource.Metadata.Annotations); } + private static bool IsNotStartedExecutableState(string? state) + { + return string.IsNullOrEmpty(state) || state == ExecutableState.Unknown; + } + public async IAsyncEnumerable> GetAllLogsAsync(string resourceName, [EnumeratorCancellation] CancellationToken cancellationToken) { IAsyncEnumerable>? enumerable = null; @@ -483,6 +497,12 @@ private async Task ProcessServiceChange(WatchEventType watchEventType, Service s return; } + if (watchEventType is WatchEventType.Added or WatchEventType.Modified && + DcpModelUtilities.TryApplyServiceAddressToEndpoint(service, _resourceState.AppResources, out var allocatedResource)) + { + await _publishEndpointsAllocatedEventAsync(allocatedResource, _shutdownToken).ConfigureAwait(false); + } + foreach (var ((resourceKind, resourceName), _) in _resourceState.ResourceAssociatedServicesMap.Where(e => e.Value.Contains(service.Metadata.Name))) { await TryRefreshResource(resourceKind, resourceName).ConfigureAwait(false); diff --git a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs index d6589eeb5d0..63e01a22f6b 100644 --- a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs +++ b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs @@ -29,6 +29,7 @@ internal sealed class ExecutableCreator : IObjectCreator _logger; private readonly DcpAppResourceStore _appResources; @@ -39,6 +40,7 @@ public ExecutableCreator( DistributedApplicationOptions distributedApplicationOptions, DistributedApplicationExecutionContext executionContext, Locations locations, + IAspireStore aspireStore, ILogger logger, DcpAppResourceStore appResources) { @@ -48,6 +50,7 @@ public ExecutableCreator( _distributedApplicationOptions = distributedApplicationOptions; _executionContext = executionContext; _locations = locations; + _aspireStore = aspireStore; _logger = logger; _appResources = appResources; } @@ -61,8 +64,8 @@ public IEnumerable> PrepareObjects() public bool IsReadyToCreate(RenderedModelResource resource, EmptyCreationContext context) { - var explicitStartup = resource.ModelResource.TryGetAnnotationsOfType(out _); - return !explicitStartup; + // Executables are always created. When explicit startup is used, DCP receives Spec.Start = false. + return true; } public async Task CreateObjectAsync(RenderedModelResource er, EmptyCreationContext context, ILogger resourceLogger, IDcpObjectFactory factory, CancellationToken cancellationToken) @@ -157,7 +160,7 @@ private void PrepareProjectExecutables() exe.Spec.WorkingDirectory = Path.GetDirectoryName(projectMetadata.ProjectPath); exe.Annotate(CustomResource.OtelServiceNameAnnotation, project.Name); - exe.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, exeInstance.Suffix); + exe.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, project.GetOtelServiceInstanceId(exeInstance)); exe.Annotate(CustomResource.ResourceNameAnnotation, project.Name); exe.Annotate(CustomResource.ResourceReplicaCount, replicas.ToString(CultureInfo.InvariantCulture)); exe.Annotate(CustomResource.ResourceReplicaIndex, i.ToString(CultureInfo.InvariantCulture)); @@ -167,8 +170,15 @@ private void PrepareProjectExecutables() var projectArgs = new List(); var isInDebugSession = !string.IsNullOrEmpty(_configuration[DcpExecutor.DebugSessionPortVar]); + var persistent = project.GetLifetimeType() == Lifetime.Persistent; + exe.Spec.Persistent = persistent; + if (persistent) + { + ApplyMonitorProcess(project, exe.Spec); + } - if (project.SupportsDebugging(_configuration, out var supportsDebuggingAnnotation)) + SupportsDebuggingAnnotation? supportsDebuggingAnnotation = null; + if (!persistent && project.SupportsDebugging(_configuration, out supportsDebuggingAnnotation)) { exe.Spec.ExecutionType = ExecutionType.IDE; exe.Spec.FallbackExecutionTypes = [ExecutionType.Process]; @@ -182,7 +192,7 @@ private void PrepareProjectExecutables() // applied later in CreateExecutableAsync() after endpoints are allocated, // unless the IDE didn't send DEBUG_SESSION_INFO (handled by the fallback branch below). } - else if (ShouldFallBackToIdeExecution(isInDebugSession, supportsDebuggingAnnotation)) + else if (!persistent && ShouldFallBackToIdeExecution(isInDebugSession, supportsDebuggingAnnotation)) { // Fall back to IDE execution with a standard ProjectLaunchConfiguration when: // 1. No SupportsDebuggingAnnotation exists (e.g. AddResource-based ProjectResource @@ -247,6 +257,11 @@ private void PrepareProjectExecutables() exe.SetAnnotationAsObjectList(CustomResource.ResourceProjectArgsAnnotation, projectArgs); + if (project.TryGetLastAnnotation(out _)) + { + exe.Spec.Start = false; + } + var exeAppResource = new RenderedModelResource(project, exe); DcpModelUtilities.AddServicesProducedInfo(exeAppResource, _appResources.Get()); _appResources.Add(exeAppResource); @@ -269,10 +284,17 @@ private void PreparePlainExecutables() // The working directory is always relative to the app host project directory (if it exists). exe.Spec.WorkingDirectory = executable.WorkingDirectory; exe.Annotate(CustomResource.OtelServiceNameAnnotation, executable.Name); - exe.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, exeInstance.Suffix); + exe.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, executable.GetOtelServiceInstanceId(exeInstance)); exe.Annotate(CustomResource.ResourceNameAnnotation, executable.Name); - if (executable.SupportsDebugging(_configuration, out _)) + var persistent = executable.GetLifetimeType() == Lifetime.Persistent; + if (persistent) + { + exe.Spec.Persistent = true; + ApplyMonitorProcess(executable, exe.Spec); + } + + if (!persistent && executable.SupportsDebugging(_configuration, out _)) { // Just mark as IDE execution here - the actual launch configuration callback // will be invoked in CreateExecutableAsync after endpoints are allocated. @@ -284,6 +306,11 @@ private void PreparePlainExecutables() exe.Spec.ExecutionType = ExecutionType.Process; } + if (executable.TryGetLastAnnotation(out _)) + { + exe.Spec.Start = false; + } + DcpExecutor.SetInitialResourceState(executable, exe); var exeAppResource = new RenderedModelResource(executable, exe); @@ -292,12 +319,20 @@ private void PreparePlainExecutables() } } + private static void ApplyMonitorProcess(IResource resource, ExecutableSpec spec) + { + if (resource.TryGetParentProcessLifetime(out var parentProcessId, out var parentProcessTimestamp)) + { + spec.MonitorPid = parentProcessId; + spec.MonitorTimestamp = parentProcessTimestamp; + } + } + private async Task BuildExecutableConfiguration(RenderedModelResource er, ILogger resourceLogger, CancellationToken cancellationToken) { var exe = (Executable)er.DcpResource; - // Build the base paths for certificate output in the DCP session directory. - var certificatesRootDir = Path.Join(_locations.DcpSessionDir, exe.Metadata.Name); + var certificatesRootDir = GetCertificatesRootDirectory(er, exe); var bundleOutputPath = Path.Join(certificatesRootDir, "cert.pem"); var customBundleOutputPath = Path.Join(certificatesRootDir, "bundles"); var certificatesOutputPath = Path.Join(certificatesRootDir, "certs"); @@ -405,6 +440,16 @@ private async Task BuildExecutableConfiguration(Rendere return (configuration, pemCertificates); } + private string GetCertificatesRootDirectory(RenderedModelResource er, Executable exe) + { + if (er.ModelResource.GetLifetimeType() == Lifetime.Persistent) + { + return Path.Join(_aspireStore.BasePath, "dcp", "executables", exe.Metadata.Name, "certificates"); + } + + return Path.Join(_locations.DcpSessionDir, exe.Metadata.Name); + } + private static List BuildLaunchArgs(RenderedModelResource er, ExecutableSpec spec, IEnumerable<(string Value, bool IsSensitive)> appHostArgs, int executableArgumentStartIndex) { // Launch args is the final list of args that are displayed in the UI and possibly added to the executable spec. diff --git a/src/Aspire.Hosting/Dcp/Model/Container.cs b/src/Aspire.Hosting/Dcp/Model/Container.cs index 7a9279632f7..470817d3230 100644 --- a/src/Aspire.Hosting/Dcp/Model/Container.cs +++ b/src/Aspire.Hosting/Dcp/Model/Container.cs @@ -62,6 +62,15 @@ internal sealed class ContainerSpec [JsonPropertyName("persistent")] public bool? Persistent { get; set; } + // Optional parent process PID used to scope persistent container cleanup to a process lifecycle. + // When set, MonitorTimestamp must also be set and Persistent must be true. + [JsonPropertyName("monitorPid")] + public int? MonitorPid { get; set; } + + // Optional parent process identity timestamp used with MonitorPid to guard against PID reuse. + [JsonPropertyName("monitorTimestamp")] + public DateTime? MonitorTimestamp { get; set; } + [JsonPropertyName("networks")] public List? Networks { get; set; } @@ -582,4 +591,3 @@ public static Container Create(string name, string image) public static string ObjectKind => Dcp.ContainerKind; } - diff --git a/src/Aspire.Hosting/Dcp/Model/Executable.cs b/src/Aspire.Hosting/Dcp/Model/Executable.cs index e56e3f38217..7491d1be005 100644 --- a/src/Aspire.Hosting/Dcp/Model/Executable.cs +++ b/src/Aspire.Hosting/Dcp/Model/Executable.cs @@ -58,8 +58,34 @@ internal sealed class ExecutableSpec public List? HealthProbes { get; set; } /// - /// Setting Stop property to true will stop the Executable if it is running. - /// Once the Executable is stopped, it cannot be started again. + /// Should this Executable be created and persisted between DCP runs? + /// Persistent executables are only compatible with the Process execution type. + /// + [JsonPropertyName("persistent")] + public bool? Persistent { get; set; } + + /// + /// Optional parent process PID used to scope persistent Executable cleanup to a process lifecycle. + /// When set, must also be set and must be true. + /// + [JsonPropertyName("monitorPid")] + public int? MonitorPid { get; set; } + + /// + /// Optional parent process identity timestamp used with to guard against PID reuse. + /// + [JsonPropertyName("monitorTimestamp")] + public DateTime? MonitorTimestamp { get; set; } + + /// + /// Should this resource be started? If set to false, we will not attempt + /// to start the resource until Start is set to true (or null). + /// + [JsonPropertyName("start")] + public bool? Start { get; set; } + + /// + /// Should this resource be stopped? /// [JsonPropertyName("stop")] public bool? Stop { get; set; } diff --git a/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs b/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs index f1337c7647b..39856c8d2ed 100644 --- a/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs +++ b/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs @@ -144,6 +144,10 @@ public CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceSn } var state = executable.AppModelInitialState is "Hidden" ? "Hidden" : executable.Status?.State; + if (executable.Spec.Start is false && IsNotStartedExecutableState(state)) + { + state = KnownResourceStates.NotStarted; + } var urls = GetUrls(executable, executable.Status?.State); @@ -206,6 +210,11 @@ public CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceSn }; } + private static bool IsNotStartedExecutableState(string? state) + { + return string.IsNullOrEmpty(state) || state == ExecutableState.Unknown; + } + private static (ImmutableArray Args, ImmutableArray? ArgsAreSensitive, bool IsSensitive)? GetLaunchArgs(CustomResource resource, IReadOnlyList? effectiveArgs) { if (!resource.TryGetAnnotationAsObjectList(CustomResource.ResourceAppArgsAnnotation, out List? launchArgumentAnnotations)) diff --git a/src/Aspire.Hosting/DistributedApplication.cs b/src/Aspire.Hosting/DistributedApplication.cs index 955e29dd090..8a299e9a673 100644 --- a/src/Aspire.Hosting/DistributedApplication.cs +++ b/src/Aspire.Hosting/DistributedApplication.cs @@ -611,6 +611,14 @@ internal async Task ExecuteBeforeStartHooksAsync(CancellationToken cancellationT } } + var logger = _host.Services.GetRequiredService>(); +#pragma warning disable CS0618 // Type or member is obsolete + if (eventing is DistributedApplicationEventing { } eventingImpl && eventingImpl.HasSubscriptions()) + { + logger.LogWarning("{EventName} is obsolete and is no longer raised by the DCP executor. Use {ResourceEventName} to observe per-resource endpoint allocation.", nameof(AfterEndpointsAllocatedEvent), nameof(ResourceEndpointsAllocatedEvent)); + } +#pragma warning restore CS0618 // Type or member is obsolete + var beforeStartEvent = new BeforeStartEvent(_host.Services, _host.Services.GetRequiredService()); using (var publishEventActivity = ProfilingTelemetry.StartAppHostPublishEvent(configuration, typeof(BeforeStartEvent))) { @@ -653,8 +661,6 @@ internal async Task ExecuteBeforeStartHooksAsync(CancellationToken cancellationT #pragma warning disable ASPIREPIPELINES001 // Pipeline APIs are experimental // Execute the before-start pipeline step var pipeline = _host.Services.GetRequiredService(); - var logger = _host.Services.GetRequiredService>(); - // Cast to internal implementation to access ExecuteStepSequentiallyAsync if (pipeline is not DistributedApplicationPipeline pipelineImpl) { diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index d5f2578541e..9097ea65c1c 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -254,7 +254,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) var appHostFilePath = options.AppHostFilePath; var assemblyMetadata = AppHostAssembly?.GetCustomAttributes(); - var aspireDir = GetMetadataValue(assemblyMetadata, "AppHostProjectBaseIntermediateOutputPath"); + var aspireDir = ResolveAspireStorePath(assemblyMetadata, AppHostDirectory); ConfigurePipelineOptions(options); @@ -993,4 +993,19 @@ private void LoadDeploymentState(string appHostSha) /// The metadata value if found; otherwise, null. private static string? GetMetadataValue(IEnumerable? assemblyMetadata, string key) => assemblyMetadata?.FirstOrDefault(a => string.Equals(a.Key, key, StringComparison.OrdinalIgnoreCase))?.Value; + + private static string ResolveAspireStorePath(IEnumerable? assemblyMetadata, string appHostDirectory) + { + var baseIntermediateOutputPath = GetMetadataValue(assemblyMetadata, "AppHostProjectBaseIntermediateOutputPath"); + if (!string.IsNullOrEmpty(baseIntermediateOutputPath)) + { + return baseIntermediateOutputPath; + } + + // File-based and dynamically loaded AppHosts do not have the MSBuild intermediate output + // metadata that normal project AppHosts get. Use the AppHost directory as the root so + // IAspireStore resolves to the workspace-local .aspire folder instead of creating a + // .NET-style obj directory for non-.NET AppHosts. + return Path.GetFullPath(appHostDirectory); + } } diff --git a/src/Aspire.Hosting/Eventing/DistributedApplicationEventing.cs b/src/Aspire.Hosting/Eventing/DistributedApplicationEventing.cs index cc11922fcfc..5bdc3f807bb 100644 --- a/src/Aspire.Hosting/Eventing/DistributedApplicationEventing.cs +++ b/src/Aspire.Hosting/Eventing/DistributedApplicationEventing.cs @@ -145,6 +145,11 @@ public DistributedApplicationEventSubscription Subscribe(Func() where T : IDistributedApplicationEvent + { + return _eventSubscriptionListLookup.TryGetValue(typeof(T), out var subscriptions) && subscriptions.Count > 0; + } + /// public DistributedApplicationEventSubscription Subscribe(IResource resource, Func callback) where T : IDistributedApplicationResourceEvent { diff --git a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs index fef561eab24..67458641902 100644 --- a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs +++ b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs @@ -135,17 +135,11 @@ private async Task WaitForInBeforeResourceStartedEvent(BeforeResourceStartedEven } } - private async Task OnEndpointsAllocated(OnEndpointsAllocatedContext context) + private Task OnEndpointsAllocated(OnEndpointsAllocatedContext context) { -#pragma warning disable CS0618 // Type or member is obsolete - var afterEndpointsAllocatedEvent = new AfterEndpointsAllocatedEvent(_serviceProvider, _model); -#pragma warning restore CS0618 // Type or member is obsolete - await _eventing.PublishAsync(afterEndpointsAllocatedEvent, context.CancellationToken).ConfigureAwait(false); - - foreach (var lifecycleHook in _lifecycleHooks) - { - await lifecycleHook.AfterEndpointsAllocatedAsync(_model, context.CancellationToken).ConfigureAwait(false); - } + // Endpoint allocation can now complete after resource creation, so there is no longer a single + // app-wide point where all endpoints are guaranteed to be allocated. + return Task.CompletedTask; } private async Task PublishResourceEndpointUrls(IResource resource, CancellationToken cancellationToken) @@ -238,7 +232,7 @@ private async Task ProcessResourceUrlCallbacks(IResource resource, CancellationT foreach (var endpoint in endpoints) { // Create a URL for each endpoint - Debug.Assert(endpoint.AllocatedEndpoint is not null, "Endpoint should be allocated at this point as we're calling this from ResourceEndpointsAllocatedEvent handler."); + Debug.Assert(endpoint.AllocatedEndpoint is not null, "Endpoint should be allocated at this point as we're processing resource endpoint allocation."); if (endpoint.AllocatedEndpoint is not { } allocatedEndpoint) { continue; diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 0825f80a763..f3ddab5429f 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Persistence annotation APIs are experimental. + using System.Diagnostics.CodeAnalysis; using System.Net.Http.Headers; using System.Net.Sockets; @@ -10,12 +12,14 @@ using Aspire.Dashboard.Model; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Ats; +using Aspire.Hosting.Dcp; using Aspire.Hosting.Dcp.Process; using Aspire.Hosting.Publishing; using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using SystemProcess = System.Diagnostics.Process; namespace Aspire.Hosting; @@ -25,8 +29,184 @@ namespace Aspire.Hosting; public static class ResourceBuilderExtensions { private const string ConnectionStringEnvironmentName = "ConnectionStrings__"; + private const string PersistenceExperimentalDiagnosticId = "ASPIREPERSISTENCE001"; private static readonly MethodInfo s_dispatchCustomWithReferenceMethod = typeof(ResourceBuilderExtensions).GetMethod(nameof(DispatchCustomWithReference), BindingFlags.NonPublic | BindingFlags.Static)!; + /// + /// Configures a resource to use a session lifetime. + /// + /// The resource type. + /// The resource builder. + /// The . + /// + /// + /// Marking a resource to have a session lifetime. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// builder.AddProject<Projects.ApiService>("api") + /// .WithSessionLifetime(); + /// + /// builder.Build().Run(); + /// + /// + /// + /// Thrown when the resource does not support lifetime configuration. + [Experimental(PersistenceExperimentalDiagnosticId, UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [AspireExport(Description = "Sets session lifetime behavior for the resource")] + public static IResourceBuilder WithSessionLifetime(this IResourceBuilder builder) + where T : IResource + { + ArgumentNullException.ThrowIfNull(builder); + + return ApplyLifetime(builder, Lifetime.Session); + } + + /// + /// Configures a resource to use a persistent lifetime. + /// + /// The resource type. + /// The resource builder. + /// The . + /// + /// + /// Marking a resource to have a persistent lifetime. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// builder.AddProject<Projects.ApiService>("api") + /// .WithPersistentLifetime(); + /// + /// builder.Build().Run(); + /// + /// + /// + /// Thrown when the resource does not support lifetime configuration. + [Experimental(PersistenceExperimentalDiagnosticId, UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [AspireExport(Description = "Sets persistent lifetime behavior for the resource")] + public static IResourceBuilder WithPersistentLifetime(this IResourceBuilder builder) + where T : IResource + { + ArgumentNullException.ThrowIfNull(builder); + + return ApplyLifetime(builder, Lifetime.Persistent); + } + + /// + /// Configures a resource to match the lifetime of another resource. + /// + /// The resource type. + /// The source resource type. + /// The resource builder. + /// The resource builder whose lifetime should be used. + /// The . + /// + /// The resource lifetime is evaluated from when the application model is prepared, so later lifetime + /// changes to the source resource are reflected by this resource. + /// + /// Thrown when the resource does not support lifetime configuration. + [Experimental(PersistenceExperimentalDiagnosticId, UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [AspireExport(Description = "Sets resource lifetime behavior to match another resource")] + public static IResourceBuilder WithLifetimeOf(this IResourceBuilder builder, IResourceBuilder sourceBuilder) + where T : IResource + where TSource : IResource + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(sourceBuilder); + + if (builder.Resource is ContainerResource or ExecutableResource or ProjectResource) + { + RemoveLegacyLifetimeAnnotations(builder); + + return builder.WithAnnotation(new PersistenceAnnotation + { + Mode = PersistenceMode.Resource, + SourceResource = sourceBuilder.Resource + }, ResourceAnnotationMutationBehavior.Replace); + } + + throw new InvalidOperationException($"Resource '{builder.Resource.Name}' does not support lifetime configuration."); + } + + /// + /// Configures a resource to use a persistent lifetime that ends when a parent process exits. + /// + /// The resource type. + /// The resource builder. + /// The ID of the parent process to monitor. + /// The . + /// + /// The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + /// + /// Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// builder.AddProject<Projects.ApiService>("api") + /// .WithParentProcessLifetime(parentProcessId: 1234); + /// + /// builder.Build().Run(); + /// + /// + /// + /// Thrown when is less than or equal to zero. + /// Thrown when does not identify a running process. + /// Thrown when the resource does not support lifetime configuration. + [Experimental(PersistenceExperimentalDiagnosticId, UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [AspireExport(Description = "Sets persistent lifetime behavior tied to a parent process")] + public static IResourceBuilder WithParentProcessLifetime(this IResourceBuilder builder, int parentProcessId) + where T : IResource + { + ArgumentNullException.ThrowIfNull(builder); + + if (parentProcessId <= 0) + { + throw new ArgumentOutOfRangeException(nameof(parentProcessId), "The parent process ID must be greater than zero."); + } + + using var parentProcess = SystemProcess.GetProcessById(parentProcessId); + var parentProcessIdentity = DcpProcessMonitor.GetMonitorProcessIdentity(parentProcess); + + RemoveLegacyLifetimeAnnotations(builder); + + return builder.WithAnnotation(new PersistenceAnnotation + { + Mode = PersistenceMode.ParentProcess, + ParentProcessId = parentProcessIdentity.ProcessId, + ParentProcessTimestamp = parentProcessIdentity.Timestamp + }, ResourceAnnotationMutationBehavior.Replace); + } + + private static IResourceBuilder ApplyLifetime(IResourceBuilder builder, Lifetime lifetime) + where T : IResource + { + if (builder.Resource is ContainerResource or ExecutableResource or ProjectResource) + { + RemoveLegacyLifetimeAnnotations(builder); + + return builder.WithAnnotation(new PersistenceAnnotation + { + Mode = lifetime switch + { + Lifetime.Session => PersistenceMode.Session, + Lifetime.Persistent => PersistenceMode.Persistent, + _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null) + } + }, ResourceAnnotationMutationBehavior.Replace); + } + + throw new InvalidOperationException($"Resource '{builder.Resource.Name}' does not support lifetime configuration."); + } + + private static void RemoveLegacyLifetimeAnnotations(IResourceBuilder builder) + where T : IResource + { + foreach (var annotation in builder.Resource.Annotations.OfType().ToArray()) + { + builder.Resource.Annotations.Remove(annotation); + } + } + /// /// Adds an environment variable to the resource. /// @@ -1347,12 +1527,12 @@ private static IResourceBuilder WithWellKnownEndpointCallback(this IResour /// An optional name of the environment variable that will be used to inject the . If the target port is null one will be dynamically generated and assigned to the environment variable. /// Indicates that this endpoint should be exposed externally at publish time. /// Network protocol: TCP or UDP are supported today, others possibly in future. - /// Specifies if the endpoint will be proxied by DCP. Defaults to true. + /// Specifies if the endpoint will be proxied by DCP. Defaults to . /// The . /// Throws an exception if an endpoint with the same name already exists on the specified resource. [AspireExport(Description = "Adds a network endpoint")] [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "")] - public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, string? scheme = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true, bool? isExternal = null, ProtocolType? protocol = null) where T : IResourceWithEndpoints + public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, string? scheme = null, [EndpointName] string? name = null, string? env = null, bool? isProxied = null, bool? isExternal = null, ProtocolType? protocol = null) where T : IResourceWithEndpoints { ArgumentNullException.ThrowIfNull(builder); @@ -1380,11 +1560,9 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build existing.IsExternal = isExternal.Value; } - // Only apply isProxied when explicitly set to false — the default is true, - // so false is always intentional and safe to apply. - if (!isProxied) + if (isProxied is not null) { - existing.IsProxied = false; + existing.IsExplicitlyProxied = isProxied; } ConfigureEndpointEnvironmentVariable(builder, existing, env); @@ -1411,6 +1589,31 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build return builder.WithAnnotation(annotation); } + /// + /// Exposes an endpoint on a resource. A reference to this endpoint can be retrieved using . + /// + /// The resource type. + /// The resource builder. + /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. + /// An optional port. This is the port that will be given to other resource to communicate with this resource. + /// An optional scheme e.g. (http/https). Defaults to the argument if it is defined or "tcp" otherwise. + /// An optional name of the endpoint. Defaults to the scheme name if not specified. + /// An optional name of the environment variable that will be used to inject the . If the target port is null one will be dynamically generated and assigned to the environment variable. + /// Indicates that this endpoint should be exposed externally at publish time. + /// Network protocol: TCP or UDP are supported today, others possibly in future. + /// Specifies if the endpoint will be proxied by DCP. + /// The . + /// Throws an exception if an endpoint with the same name already exists on the specified resource. + /// + /// This overload preserves binary compatibility for callers compiled against the previous signature. + /// New source that omits binds to the nullable overload where omission is represented as . + /// + [AspireExportIgnore(Reason = "Binary compatibility shim for the nullable isProxied overload.")] + public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port, int? targetPort, string? scheme, [EndpointName] string? name, string? env, bool isProxied, bool? isExternal, ProtocolType? protocol) where T : IResourceWithEndpoints + { + return WithEndpoint(builder, port, targetPort, scheme, name, env, (bool?)isProxied, isExternal, protocol); + } + /// /// Configures the environment variable callback for an endpoint's target port. /// If a callback already exists (from a prior call), the annotation's @@ -1443,6 +1646,34 @@ private static void ConfigureEndpointEnvironmentVariable(IResourceBuilder })); } + /// + /// Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + /// If set to false, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + /// + /// The resource builder. + /// Should endpoints for the resource support using a proxy? + /// The resource builder. + /// + /// This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + /// port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + /// The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + /// endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + /// + [AspireExport(Description = "Configures endpoint proxy support")] + public static IResourceBuilder WithEndpointProxySupport(this IResourceBuilder builder, bool proxyEnabled) + { + return SetEndpointProxySupport(builder, proxyEnabled); + } + + internal static IResourceBuilder SetEndpointProxySupport(IResourceBuilder builder, bool proxyEnabled) where T : IResourceWithEndpoints + { + ArgumentNullException.ThrowIfNull(builder); + + builder.WithAnnotation(new ProxySupportAnnotation { ProxyEnabled = proxyEnabled }, ResourceAnnotationMutationBehavior.Replace); + + return builder; + } + /// /// Exposes an endpoint on a resource. This endpoint reference can be retrieved using . /// The endpoint name will be the scheme name if not specified. @@ -1455,7 +1686,7 @@ private static void ConfigureEndpointEnvironmentVariable(IResourceBuilder /// An optional name of the endpoint. Defaults to the scheme name if not specified. /// An optional name of the environment variable that will be used to inject the . If the target port is null one will be dynamically generated and assigned to the environment variable. /// Indicates that this endpoint should be exposed externally at publish time. - /// Specifies if the endpoint will be proxied by DCP. Defaults to true. + /// Specifies if the endpoint will be proxied by DCP. Defaults to . /// The . /// Throws an exception if an endpoint with the same name already exists on the specified resource. /// @@ -1463,11 +1694,36 @@ private static void ConfigureEndpointEnvironmentVariable(IResourceBuilder /// If an endpoint with the same name already exists, the existing endpoint is updated with any non-null parameter values. /// [AspireExportIgnore(Reason = "Subset of the full WithEndpoint overload which is already exported.")] - public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port, int? targetPort, string? scheme, [EndpointName] string? name, string? env, bool isProxied, bool? isExternal) where T : IResourceWithEndpoints + public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port, int? targetPort, string? scheme, [EndpointName] string? name, string? env, bool? isProxied, bool? isExternal) where T : IResourceWithEndpoints { return WithEndpoint(builder, port, targetPort, scheme, name, env, isProxied, isExternal, protocol: null); } + /// + /// Exposes an endpoint on a resource. This endpoint reference can be retrieved using . + /// The endpoint name will be the scheme name if not specified. + /// + /// The resource type. + /// The resource builder. + /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. + /// An optional port. This is the port that will be given to other resource to communicate with this resource. + /// An optional scheme e.g. (http/https). Defaults to "tcp" if not specified. + /// An optional name of the endpoint. Defaults to the scheme name if not specified. + /// An optional name of the environment variable that will be used to inject the . If the target port is null one will be dynamically generated and assigned to the environment variable. + /// Indicates that this endpoint should be exposed externally at publish time. + /// Specifies if the endpoint will be proxied by DCP. + /// The . + /// Throws an exception if an endpoint with the same name already exists on the specified resource. + /// + /// This overload preserves binary compatibility for callers compiled against the previous signature. + /// New source that omits binds to the nullable overload where omission is represented as . + /// + [AspireExportIgnore(Reason = "Binary compatibility shim for the nullable isProxied overload.")] + public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port, int? targetPort, string? scheme, [EndpointName] string? name, string? env, bool isProxied, bool? isExternal) where T : IResourceWithEndpoints + { + return WithEndpoint(builder, port, targetPort, scheme, name, env, (bool?)isProxied, isExternal, protocol: null); + } + /// /// Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. /// This endpoint reference can be retrieved using . @@ -1479,20 +1735,41 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build /// An optional port. This is the port that will be given to other resource to communicate with this resource. /// An optional name of the endpoint. Defaults to "http" if not specified. /// An optional name of the environment variable to inject. - /// Specifies if the endpoint will be proxied by DCP. Defaults to true. + /// Specifies if the endpoint will be proxied by DCP. Defaults to . /// The . /// /// If an endpoint with the same name already exists on the resource, the existing endpoint is updated /// with any non-null parameter values. Parameters left as will not modify the existing endpoint's values. /// [AspireExport(Description = "Adds an HTTP endpoint")] - public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true) where T : IResourceWithEndpoints + public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool? isProxied = null) where T : IResourceWithEndpoints { ArgumentNullException.ThrowIfNull(builder); return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "http", name: name, env: env, isProxied: isProxied); } + /// + /// Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. + /// + /// The resource type. + /// The resource builder. + /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. + /// An optional port. This is the port that will be given to other resource to communicate with this resource. + /// An optional name of the endpoint. Defaults to "http" if not specified. + /// An optional name of the environment variable to inject. + /// Specifies if the endpoint will be proxied by DCP. + /// The . + /// + /// This overload preserves binary compatibility for callers compiled against the previous signature. + /// New source that omits binds to the nullable overload where omission is represented as . + /// + [AspireExportIgnore(Reason = "Binary compatibility shim for the nullable isProxied overload.")] + public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder builder, int? port, int? targetPort, [EndpointName] string? name, string? env, bool isProxied) where T : IResourceWithEndpoints + { + return WithHttpEndpoint(builder, port, targetPort, name, env, (bool?)isProxied); + } + /// /// Exposes an HTTPS endpoint on a resource, or updates the existing HTTPS endpoint if one with the same name already exists. /// This endpoint reference can be retrieved using . @@ -1504,20 +1781,41 @@ public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder b /// An optional host port. /// An optional name of the endpoint. Defaults to "https" if not specified. /// An optional name of the environment variable to inject. - /// Specifies if the endpoint will be proxied by DCP. Defaults to true. + /// Specifies if the endpoint will be proxied by DCP. Defaults to . /// The . /// /// If an endpoint with the same name already exists on the resource, the existing endpoint is updated /// with any non-null parameter values. Parameters left as will not modify the existing endpoint's values. /// [AspireExport(Description = "Adds an HTTPS endpoint")] - public static IResourceBuilder WithHttpsEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true) where T : IResourceWithEndpoints + public static IResourceBuilder WithHttpsEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool? isProxied = null) where T : IResourceWithEndpoints { ArgumentNullException.ThrowIfNull(builder); return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "https", name: name, env: env, isProxied: isProxied); } + /// + /// Exposes an HTTPS endpoint on a resource, or updates the existing HTTPS endpoint if one with the same name already exists. + /// + /// The resource type. + /// The resource builder. + /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. + /// An optional host port. + /// An optional name of the endpoint. Defaults to "https" if not specified. + /// An optional name of the environment variable to inject. + /// Specifies if the endpoint will be proxied by DCP. + /// The . + /// + /// This overload preserves binary compatibility for callers compiled against the previous signature. + /// New source that omits binds to the nullable overload where omission is represented as . + /// + [AspireExportIgnore(Reason = "Binary compatibility shim for the nullable isProxied overload.")] + public static IResourceBuilder WithHttpsEndpoint(this IResourceBuilder builder, int? port, int? targetPort, [EndpointName] string? name, string? env, bool isProxied) where T : IResourceWithEndpoints + { + return WithHttpsEndpoint(builder, port, targetPort, name, env, (bool?)isProxied); + } + /// /// Marks existing http or https endpoints on a resource as external. /// @@ -1546,7 +1844,7 @@ public static IResourceBuilder WithExternalHttpEndpoints(this IResourceBui } /// - /// Gets an by name from the resource. These endpoints are declared either using or by launch settings (for project resources). + /// Gets an by name from the resource. These endpoints are declared either using or by launch settings (for project resources). /// The can be used to resolve the address of the endpoint in . /// /// The resource type. @@ -1564,7 +1862,7 @@ public static EndpointReference GetEndpoint(this IResourceBuilder builder, } /// - /// Gets an by name from the resource. These endpoints are declared either using or by launch settings (for project resources). + /// Gets an by name from the resource. These endpoints are declared either using or by launch settings (for project resources). /// The can be used to resolve the address of the endpoint in . /// /// The resource type. @@ -2462,6 +2760,7 @@ public static IResourceBuilder WithHttpHealthCheck(this IResourceBuilder { if (!endpoint.Exists) @@ -2469,12 +2768,6 @@ public static IResourceBuilder WithHttpHealthCheck(this IResourceBuilder - { var baseUri = new Uri(endpoint.Url, UriKind.Absolute); uri = new Uri(baseUri, path); return Task.CompletedTask; diff --git a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt index 2c4a155c267..829545e7538 100644 --- a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt +++ b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt @@ -620,7 +620,7 @@ Aspire.Hosting/withDockerfileBaseImage(buildImage?: string, runtimeImage?: strin Aspire.Hosting/withDockerfileBuilder(contextPath: string, callback: callback, stage?: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource Aspire.Hosting/withEndpoint(port?: number, targetPort?: number, scheme?: string, name?: string, env?: string, isProxied?: boolean, isExternal?: boolean, protocol?: enum:System.Net.Sockets.ProtocolType) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints Aspire.Hosting/withEndpointCallback(endpointName: string, callback: callback, createIfNotExists?: boolean) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints -Aspire.Hosting/withEndpointProxySupport(proxyEnabled: boolean) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource +Aspire.Hosting/withEndpointProxySupport(proxyEnabled: boolean) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints Aspire.Hosting/withEntrypoint(entrypoint: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource Aspire.Hosting/withEnvironment(name: string, value: string|Aspire.Hosting/Aspire.Hosting.ApplicationModel.ReferenceExpression|Aspire.Hosting/Aspire.Hosting.ApplicationModel.EndpointReference|Aspire.Hosting/Aspire.Hosting.ApplicationModel.ParameterResource|Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithConnectionString|Aspire.Hosting/Aspire.Hosting.ApplicationModel.IExpressionValue) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment Aspire.Hosting/withEnvironmentCallback(callback: callback) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment @@ -646,11 +646,14 @@ Aspire.Hosting/withImageRegistry(registry: string) -> Aspire.Hosting/Aspire.Host Aspire.Hosting/withImageSHA256(sha256: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource Aspire.Hosting/withImageTag(tag: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource Aspire.Hosting/withLifetime(lifetime: enum:Aspire.Hosting.ApplicationModel.ContainerLifetime) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource +Aspire.Hosting/withLifetimeOf(sourceBuilder: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource Aspire.Hosting/withMcpServer(path?: string, endpointName?: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints Aspire.Hosting/withOtlpExporter(protocol?: enum:Aspire.Hosting.OtlpProtocol) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment Aspire.Hosting/withoutHttpsCertificate() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment Aspire.Hosting/withParameterBuildSecret(name: string, value: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ParameterResource) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource Aspire.Hosting/withParameterHttpsDeveloperCertificate(password?: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ParameterResource) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment +Aspire.Hosting/withParentProcessLifetime(parentProcessId: number) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource +Aspire.Hosting/withPersistentLifetime() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource Aspire.Hosting/withPipelineConfiguration(callback: callback) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource Aspire.Hosting/withPipelineStepFactory(stepName: string, callback: callback, dependsOn?: string[], requiredBy?: string[], tags?: string[], description?: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource Aspire.Hosting/withProcessCommand(commandName: string, displayName: string, options: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ProcessCommandExportOptions) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource @@ -661,6 +664,7 @@ Aspire.Hosting/withRemoteImageName(remoteImageName: string) -> Aspire.Hosting/As Aspire.Hosting/withRemoteImageTag(remoteImageTag: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IComputeResource Aspire.Hosting/withReplicas(replicas: number) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ProjectResource Aspire.Hosting/withRequiredCommand(command: string, helpLink?: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource +Aspire.Hosting/withSessionLifetime() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource Aspire.Hosting/withToolIgnoreExistingFeeds() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.DotnetToolResource Aspire.Hosting/withToolIgnoreFailedSources() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.DotnetToolResource Aspire.Hosting/withToolPackage(packageId: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.DotnetToolResource diff --git a/src/Aspire.Hosting/api/Aspire.Hosting.tscompat.suppression.txt b/src/Aspire.Hosting/api/Aspire.Hosting.tscompat.suppression.txt new file mode 100644 index 00000000000..b0628ee3e5e --- /dev/null +++ b/src/Aspire.Hosting/api/Aspire.Hosting.tscompat.suppression.txt @@ -0,0 +1 @@ +BREAK capability-return-type-changed Aspire.Hosting Aspire.Hosting/withEndpointProxySupport -- https://github.com/microsoft/aspire/pull/17112 -- Generalized endpoint proxy support from containers to all endpoint-capable resources while preserving source-level TypeScript compatibility. diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptSqlServerNativeAssetsBundleTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptSqlServerNativeAssetsBundleTests.cs index 0492cd3a386..d3ba76ec212 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptSqlServerNativeAssetsBundleTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptSqlServerNativeAssetsBundleTests.cs @@ -56,11 +56,11 @@ public async Task StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets() var appHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.ts"); File.WriteAllText(appHostPath, """ - import { createBuilder, ContainerLifetime } from './.modules/aspire.js'; + import { createBuilder } from './.modules/aspire.js'; const builder = await createBuilder(); const sql = await builder.addSqlServer('sql') - .withLifetime(ContainerLifetime.Persistent) + .withPersistentLifetime() .withDataVolume(); await sql.addDatabase('mydb'); diff --git a/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs b/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs index b029174d695..2560c5455a3 100644 --- a/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs +++ b/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Testing; using Aspire.Hosting.Utils; @@ -295,7 +297,7 @@ public void RunAsEmulator_WithCustomImage_ShouldUseSpecifiedValues() } [Fact] - public void RunAsEmulator_WithCustomLifetime_ShouldConfigureLifetimeAnnotation() + public void RunAsEmulator_WithCustomLifetime_ShouldConfigurePersistenceAnnotation() { // Arrange using var builder = TestDistributedApplicationBuilder.Create(); @@ -303,13 +305,12 @@ public void RunAsEmulator_WithCustomLifetime_ShouldConfigureLifetimeAnnotation() // Act var resourceBuilder = builder.AddAzureKustoCluster("test-kusto").RunAsEmulator(containerBuilder => { - containerBuilder.WithLifetime(ContainerLifetime.Persistent); + containerBuilder.WithPersistentLifetime(); }); // Assert - var lifetimeAnnotation = resourceBuilder.Resource.Annotations.OfType().SingleOrDefault(); - Assert.NotNull(lifetimeAnnotation); - Assert.Equal(ContainerLifetime.Persistent, lifetimeAnnotation.Lifetime); + var persistenceAnnotation = Assert.Single(resourceBuilder.Resource.Annotations.OfType()); + Assert.Equal(PersistenceMode.Persistent, persistenceAnnotation.Mode); } [Fact] diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs index e974b387335..994756fddd2 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + using System.Text; using System.Text.Json.Nodes; using Aspire.Hosting.ApplicationModel; @@ -507,22 +509,40 @@ public async Task AzureEventHubsEmulator_WithConfigurationFile() public void AddAzureEventHubsWithEmulator_SetsStorageLifetime(bool isPersistent) { using var builder = TestDistributedApplicationBuilder.Create(); - var lifetime = isPersistent ? ContainerLifetime.Persistent : ContainerLifetime.Session; + var lifetime = isPersistent ? Lifetime.Persistent : Lifetime.Session; - var serviceBus = builder.AddAzureEventHubs("eh").RunAsEmulator(configureContainer: builder => + var eventHubs = builder.AddAzureEventHubs("eh").RunAsEmulator(configureContainer: builder => { - builder.WithLifetime(lifetime); + _ = lifetime switch + { + Lifetime.Session => builder.WithSessionLifetime(), + Lifetime.Persistent => builder.WithPersistentLifetime(), + _ => throw new InvalidOperationException($"Unknown resource lifetime '{Enum.GetName(typeof(Lifetime), lifetime)}'.") + }; }); var azurite = builder.Resources.FirstOrDefault(x => x.Name == "eh-storage"); Assert.NotNull(azurite); - serviceBus.Resource.TryGetLastAnnotation(out var sbLifetimeAnnotation); - azurite.TryGetLastAnnotation(out var sqlLifetimeAnnotation); + var sourceResource = GetPersistenceReferenceSource(azurite); + Assert.Same(eventHubs.Resource.Annotations, sourceResource.Annotations); + + var persistenceAnnotation = Assert.Single(eventHubs.Resource.Annotations.OfType()); + Assert.Equal(ToPersistenceMode(lifetime), persistenceAnnotation.Mode); + } + + [Fact] + public void AddAzureEventHubsWithEmulator_DoesNotSetStorageLifetimeWithoutContainerConfiguration() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + builder.AddAzureEventHubs("eh").RunAsEmulator(); + + var azurite = builder.Resources.FirstOrDefault(x => x.Name == "eh-storage"); - Assert.Equal(lifetime, sbLifetimeAnnotation?.Lifetime); - Assert.Equal(lifetime, sqlLifetimeAnnotation?.Lifetime); + Assert.NotNull(azurite); + Assert.Empty(azurite.Annotations.OfType()); } [Fact] @@ -534,6 +554,20 @@ public void RunAsEmulator_CalledTwice_Throws() Assert.Throws(() => eventHubs.RunAsEmulator()); } + private static IResource GetPersistenceReferenceSource(IResource resource) + { + var annotation = Assert.Single(resource.Annotations.OfType()); + return Assert.IsAssignableFrom(annotation.SourceResource); + } + + private static PersistenceMode ToPersistenceMode(Lifetime lifetime) => + lifetime switch + { + Lifetime.Session => PersistenceMode.Session, + Lifetime.Persistent => PersistenceMode.Persistent, + _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null) + }; + [Fact] public void AzureEventHubsHasCorrectConnectionStrings() { diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureResourceOptionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureResourceOptionsTests.cs index f172fc6014c..8e8bb7a6e1b 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureResourceOptionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureResourceOptionsTests.cs @@ -1,7 +1,8 @@ // 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.ApplicationModel; +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -34,7 +35,7 @@ public async Task AzureResourceOptionsCanBeConfigured() // ensure that resources with a hyphen still have a hyphen in the bicep name var sqlDatabase = builder.AddAzureSqlServer("sql-server") - .RunAsContainer(x => x.WithLifetime(ContainerLifetime.Persistent)) + .RunAsContainer(x => x.WithPersistentLifetime()) .AddDatabase("evadexdb").WithDefaultAzureSku(); using var app = builder.Build(); diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs index 20320a5eb5d..b8f24faa8ef 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + using System.Text.Json.Nodes; using System.Reflection; using Aspire.Hosting.ApplicationModel; @@ -604,22 +606,40 @@ public async Task AzureServiceBusEmulator_WithConfigurationFile() public void AddAzureServiceBusWithEmulator_SetsSqlLifetime(bool isPersistent) { using var builder = TestDistributedApplicationBuilder.Create(); - var lifetime = isPersistent ? ContainerLifetime.Persistent : ContainerLifetime.Session; + var lifetime = isPersistent ? Lifetime.Persistent : Lifetime.Session; var serviceBus = builder.AddAzureServiceBus("sb").RunAsEmulator(configureContainer: builder => { - builder.WithLifetime(lifetime); + _ = lifetime switch + { + Lifetime.Session => builder.WithSessionLifetime(), + Lifetime.Persistent => builder.WithPersistentLifetime(), + _ => throw new InvalidOperationException($"Unknown resource lifetime '{Enum.GetName(typeof(Lifetime), lifetime)}'.") + }; }); var sql = builder.Resources.FirstOrDefault(x => x.Name == "sb-mssql"); Assert.NotNull(sql); - serviceBus.Resource.TryGetLastAnnotation(out var sbLifetimeAnnotation); - sql.TryGetLastAnnotation(out var sqlLifetimeAnnotation); + var sourceResource = GetPersistenceReferenceSource(sql); + Assert.Same(serviceBus.Resource.Annotations, sourceResource.Annotations); - Assert.Equal(lifetime, sbLifetimeAnnotation?.Lifetime); - Assert.Equal(lifetime, sqlLifetimeAnnotation?.Lifetime); + var persistenceAnnotation = Assert.Single(serviceBus.Resource.Annotations.OfType()); + Assert.Equal(ToPersistenceMode(lifetime), persistenceAnnotation.Mode); + } + + [Fact] + public void AddAzureServiceBusWithEmulator_DoesNotSetSqlLifetimeWithoutContainerConfiguration() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + builder.AddAzureServiceBus("sb").RunAsEmulator(); + + var sql = builder.Resources.FirstOrDefault(x => x.Name == "sb-mssql"); + + Assert.NotNull(sql); + Assert.Empty(sql.Annotations.OfType()); } [Fact] @@ -631,6 +651,20 @@ public void RunAsEmulator_CalledTwice_Throws() Assert.Throws(() => serviceBus.RunAsEmulator()); } + private static IResource GetPersistenceReferenceSource(IResource resource) + { + var annotation = Assert.Single(resource.Annotations.OfType()); + return Assert.IsAssignableFrom(annotation.SourceResource); + } + + private static PersistenceMode ToPersistenceMode(Lifetime lifetime) => + lifetime switch + { + Lifetime.Session => PersistenceMode.Session, + Lifetime.Persistent => PersistenceMode.Persistent, + _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null) + }; + [Fact] public void AzureServiceBusHasCorrectConnectionStrings() { diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index e7bb5fb7a68..2440ac457c8 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -1106,6 +1106,7 @@ type Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource interface { WithImageSHA256(sha256 string) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithImageTag(tag string) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithLifetime(lifetime ContainerLifetime) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource + WithLifetimeOf(sourceBuilder Resource) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithMcpServer(options ...*WithMcpServerOptions) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithMergeEndpoint(endpointName string, port float64) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource @@ -1120,7 +1121,9 @@ type Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource interface { WithOptionalCallback(options ...*WithOptionalCallbackOptions) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithOptionalString(options ...*WithOptionalStringOptions) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithOtlpExporter(options ...*WithOtlpExporterOptions) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource + WithParentProcessLifetime(parentProcessId float64) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithParentRelationship(parent Resource) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource + WithPersistentLifetime() Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource @@ -1131,6 +1134,7 @@ type Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource interface { WithRemoteImageName(remoteImageName string) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithRemoteImageTag(remoteImageTag string) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource + WithSessionLifetime() Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithStatus(status TestResourceStatus) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithUnionDependency(dependency any) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithUrl(url any, options ...*WithUrlOptions) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource @@ -2256,6 +2260,19 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithLifetime(l return s } +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithLifetimeOf(sourceBuilder Resource) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource { + if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithMcpServer configures an MCP server endpoint on the resource func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithMcpServer(options ...*WithMcpServerOptions) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource { if s.err != nil { return s } @@ -2482,6 +2499,18 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithOtlpExport return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithParentProcessLifetime(parentProcessId float64) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithParentRelationship(parent Resource) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource { if s.err != nil { return s } @@ -2495,6 +2524,17 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithParentRela return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithPersistentLifetime() Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource { if s.err != nil { return s } @@ -2677,6 +2717,17 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithRequiredCo return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithSessionLifetime() Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithStatus(status TestResourceStatus) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource { if s.err != nil { return s } @@ -3040,6 +3091,7 @@ type CSharpAppResource interface { WithDockerfileBaseImage(options ...*WithDockerfileBaseImageOptions) CSharpAppResource WithEndpoint(options ...*WithEndpointOptions) CSharpAppResource WithEndpointCallback(endpointName string, callback func(obj EndpointUpdateContext), options ...*WithEndpointCallbackOptions) CSharpAppResource + WithEndpointProxySupport(proxyEnabled bool) CSharpAppResource WithEndpoints(endpoints []string) CSharpAppResource WithEnvironment(name string, value any) CSharpAppResource WithEnvironmentCallback(callback func(arg EnvironmentCallbackContext)) CSharpAppResource @@ -3057,6 +3109,7 @@ type CSharpAppResource interface { WithHttpsEndpointCallback(callback func(obj EndpointUpdateContext), options ...*WithHttpsEndpointCallbackOptions) CSharpAppResource WithIconName(iconName string, options ...*WithIconNameOptions) CSharpAppResource WithImagePushOptions(callback func(arg ContainerImagePushOptionsCallbackContext)) CSharpAppResource + WithLifetimeOf(sourceBuilder Resource) CSharpAppResource WithMcpServer(options ...*WithMcpServerOptions) CSharpAppResource WithMergeEndpoint(endpointName string, port float64) CSharpAppResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) CSharpAppResource @@ -3071,7 +3124,9 @@ type CSharpAppResource interface { WithOptionalCallback(options ...*WithOptionalCallbackOptions) CSharpAppResource WithOptionalString(options ...*WithOptionalStringOptions) CSharpAppResource WithOtlpExporter(options ...*WithOtlpExporterOptions) CSharpAppResource + WithParentProcessLifetime(parentProcessId float64) CSharpAppResource WithParentRelationship(parent Resource) CSharpAppResource + WithPersistentLifetime() CSharpAppResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) CSharpAppResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) CSharpAppResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) CSharpAppResource @@ -3083,6 +3138,7 @@ type CSharpAppResource interface { WithRemoteImageTag(remoteImageTag string) CSharpAppResource WithReplicas(replicas float64) CSharpAppResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) CSharpAppResource + WithSessionLifetime() CSharpAppResource WithStatus(status TestResourceStatus) CSharpAppResource WithUnionDependency(dependency any) CSharpAppResource WithUrl(url any, options ...*WithUrlOptions) CSharpAppResource @@ -3680,6 +3736,18 @@ func (s *cSharpAppResource) WithEndpointCallback(endpointName string, callback f return s } +// WithEndpointProxySupport configures endpoint proxy support +func (s *cSharpAppResource) WithEndpointProxySupport(proxyEnabled bool) CSharpAppResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["proxyEnabled"] = serializeValue(proxyEnabled) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withEndpointProxySupport", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithEndpoints sets the endpoints func (s *cSharpAppResource) WithEndpoints(endpoints []string) CSharpAppResource { if s.err != nil { return s } @@ -3978,6 +4046,19 @@ func (s *cSharpAppResource) WithImagePushOptions(callback func(arg ContainerImag return s } +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *cSharpAppResource) WithLifetimeOf(sourceBuilder Resource) CSharpAppResource { + if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithMcpServer configures an MCP server endpoint on the resource func (s *cSharpAppResource) WithMcpServer(options ...*WithMcpServerOptions) CSharpAppResource { if s.err != nil { return s } @@ -4204,6 +4285,18 @@ func (s *cSharpAppResource) WithOtlpExporter(options ...*WithOtlpExporterOptions return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *cSharpAppResource) WithParentProcessLifetime(parentProcessId float64) CSharpAppResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *cSharpAppResource) WithParentRelationship(parent Resource) CSharpAppResource { if s.err != nil { return s } @@ -4217,6 +4310,17 @@ func (s *cSharpAppResource) WithParentRelationship(parent Resource) CSharpAppRes return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *cSharpAppResource) WithPersistentLifetime() CSharpAppResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *cSharpAppResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) CSharpAppResource { if s.err != nil { return s } @@ -4411,6 +4515,17 @@ func (s *cSharpAppResource) WithRequiredCommand(command string, options ...*With return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *cSharpAppResource) WithSessionLifetime() CSharpAppResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *cSharpAppResource) WithStatus(status TestResourceStatus) CSharpAppResource { if s.err != nil { return s } @@ -5253,6 +5368,7 @@ type ContainerRegistryResource interface { WithExplicitStart() ContainerRegistryResource WithHealthCheck(key string) ContainerRegistryResource WithIconName(iconName string, options ...*WithIconNameOptions) ContainerRegistryResource + WithLifetimeOf(sourceBuilder Resource) ContainerRegistryResource WithMergeEndpoint(endpointName string, port float64) ContainerRegistryResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) ContainerRegistryResource WithMergeLabel(label string) ContainerRegistryResource @@ -5265,13 +5381,16 @@ type ContainerRegistryResource interface { WithNestedConfig(config *TestNestedDto) ContainerRegistryResource WithOptionalCallback(options ...*WithOptionalCallbackOptions) ContainerRegistryResource WithOptionalString(options ...*WithOptionalStringOptions) ContainerRegistryResource + WithParentProcessLifetime(parentProcessId float64) ContainerRegistryResource WithParentRelationship(parent Resource) ContainerRegistryResource + WithPersistentLifetime() ContainerRegistryResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ContainerRegistryResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) ContainerRegistryResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) ContainerRegistryResource WithProcessCommandFactory(commandName string, displayName string, createProcessSpec func(arg ExecuteCommandContext) *ProcessCommandSpecExportData, options ...*WithProcessCommandFactoryOptions) ContainerRegistryResource WithRelationship(resourceBuilder Resource, type_ string) ContainerRegistryResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) ContainerRegistryResource + WithSessionLifetime() ContainerRegistryResource WithStatus(status TestResourceStatus) ContainerRegistryResource WithUnionDependency(dependency any) ContainerRegistryResource WithUrl(url any, options ...*WithUrlOptions) ContainerRegistryResource @@ -5629,6 +5748,19 @@ func (s *containerRegistryResource) WithIconName(iconName string, options ...*Wi return s } +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *containerRegistryResource) WithLifetimeOf(sourceBuilder Resource) ContainerRegistryResource { + if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithMergeEndpoint configures a named endpoint func (s *containerRegistryResource) WithMergeEndpoint(endpointName string, port float64) ContainerRegistryResource { if s.err != nil { return s } @@ -5819,6 +5951,18 @@ func (s *containerRegistryResource) WithOptionalString(options ...*WithOptionalS return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *containerRegistryResource) WithParentProcessLifetime(parentProcessId float64) ContainerRegistryResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *containerRegistryResource) WithParentRelationship(parent Resource) ContainerRegistryResource { if s.err != nil { return s } @@ -5832,6 +5976,17 @@ func (s *containerRegistryResource) WithParentRelationship(parent Resource) Cont return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *containerRegistryResource) WithPersistentLifetime() ContainerRegistryResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *containerRegistryResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ContainerRegistryResource { if s.err != nil { return s } @@ -5952,6 +6107,17 @@ func (s *containerRegistryResource) WithRequiredCommand(command string, options return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *containerRegistryResource) WithSessionLifetime() ContainerRegistryResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *containerRegistryResource) WithStatus(status TestResourceStatus) ContainerRegistryResource { if s.err != nil { return s } @@ -6140,6 +6306,7 @@ type ContainerResource interface { WithImageSHA256(sha256 string) ContainerResource WithImageTag(tag string) ContainerResource WithLifetime(lifetime ContainerLifetime) ContainerResource + WithLifetimeOf(sourceBuilder Resource) ContainerResource WithMcpServer(options ...*WithMcpServerOptions) ContainerResource WithMergeEndpoint(endpointName string, port float64) ContainerResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) ContainerResource @@ -6154,7 +6321,9 @@ type ContainerResource interface { WithOptionalCallback(options ...*WithOptionalCallbackOptions) ContainerResource WithOptionalString(options ...*WithOptionalStringOptions) ContainerResource WithOtlpExporter(options ...*WithOtlpExporterOptions) ContainerResource + WithParentProcessLifetime(parentProcessId float64) ContainerResource WithParentRelationship(parent Resource) ContainerResource + WithPersistentLifetime() ContainerResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ContainerResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) ContainerResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) ContainerResource @@ -6165,6 +6334,7 @@ type ContainerResource interface { WithRemoteImageName(remoteImageName string) ContainerResource WithRemoteImageTag(remoteImageTag string) ContainerResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) ContainerResource + WithSessionLifetime() ContainerResource WithStatus(status TestResourceStatus) ContainerResource WithUnionDependency(dependency any) ContainerResource WithUrl(url any, options ...*WithUrlOptions) ContainerResource @@ -7289,6 +7459,19 @@ func (s *containerResource) WithLifetime(lifetime ContainerLifetime) ContainerRe return s } +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *containerResource) WithLifetimeOf(sourceBuilder Resource) ContainerResource { + if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithMcpServer configures an MCP server endpoint on the resource func (s *containerResource) WithMcpServer(options ...*WithMcpServerOptions) ContainerResource { if s.err != nil { return s } @@ -7515,6 +7698,18 @@ func (s *containerResource) WithOtlpExporter(options ...*WithOtlpExporterOptions return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *containerResource) WithParentProcessLifetime(parentProcessId float64) ContainerResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *containerResource) WithParentRelationship(parent Resource) ContainerResource { if s.err != nil { return s } @@ -7528,6 +7723,17 @@ func (s *containerResource) WithParentRelationship(parent Resource) ContainerRes return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *containerResource) WithPersistentLifetime() ContainerResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *containerResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ContainerResource { if s.err != nil { return s } @@ -7710,6 +7916,17 @@ func (s *containerResource) WithRequiredCommand(command string, options ...*With return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *containerResource) WithSessionLifetime() ContainerResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *containerResource) WithStatus(status TestResourceStatus) ContainerResource { if s.err != nil { return s } @@ -9389,6 +9606,7 @@ type DotnetToolResource interface { WithDockerfileBaseImage(options ...*WithDockerfileBaseImageOptions) DotnetToolResource WithEndpoint(options ...*WithEndpointOptions) DotnetToolResource WithEndpointCallback(endpointName string, callback func(obj EndpointUpdateContext), options ...*WithEndpointCallbackOptions) DotnetToolResource + WithEndpointProxySupport(proxyEnabled bool) DotnetToolResource WithEndpoints(endpoints []string) DotnetToolResource WithEnvironment(name string, value any) DotnetToolResource WithEnvironmentCallback(callback func(arg EnvironmentCallbackContext)) DotnetToolResource @@ -9407,6 +9625,7 @@ type DotnetToolResource interface { WithHttpsEndpointCallback(callback func(obj EndpointUpdateContext), options ...*WithHttpsEndpointCallbackOptions) DotnetToolResource WithIconName(iconName string, options ...*WithIconNameOptions) DotnetToolResource WithImagePushOptions(callback func(arg ContainerImagePushOptionsCallbackContext)) DotnetToolResource + WithLifetimeOf(sourceBuilder Resource) DotnetToolResource WithMcpServer(options ...*WithMcpServerOptions) DotnetToolResource WithMergeEndpoint(endpointName string, port float64) DotnetToolResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) DotnetToolResource @@ -9421,7 +9640,9 @@ type DotnetToolResource interface { WithOptionalCallback(options ...*WithOptionalCallbackOptions) DotnetToolResource WithOptionalString(options ...*WithOptionalStringOptions) DotnetToolResource WithOtlpExporter(options ...*WithOtlpExporterOptions) DotnetToolResource + WithParentProcessLifetime(parentProcessId float64) DotnetToolResource WithParentRelationship(parent Resource) DotnetToolResource + WithPersistentLifetime() DotnetToolResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) DotnetToolResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) DotnetToolResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) DotnetToolResource @@ -9432,6 +9653,7 @@ type DotnetToolResource interface { WithRemoteImageName(remoteImageName string) DotnetToolResource WithRemoteImageTag(remoteImageTag string) DotnetToolResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) DotnetToolResource + WithSessionLifetime() DotnetToolResource WithStatus(status TestResourceStatus) DotnetToolResource WithToolIgnoreExistingFeeds() DotnetToolResource WithToolIgnoreFailedSources() DotnetToolResource @@ -10004,6 +10226,18 @@ func (s *dotnetToolResource) WithEndpointCallback(endpointName string, callback return s } +// WithEndpointProxySupport configures endpoint proxy support +func (s *dotnetToolResource) WithEndpointProxySupport(proxyEnabled bool) DotnetToolResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["proxyEnabled"] = serializeValue(proxyEnabled) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withEndpointProxySupport", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithEndpoints sets the endpoints func (s *dotnetToolResource) WithEndpoints(endpoints []string) DotnetToolResource { if s.err != nil { return s } @@ -10314,6 +10548,19 @@ func (s *dotnetToolResource) WithImagePushOptions(callback func(arg ContainerIma return s } +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *dotnetToolResource) WithLifetimeOf(sourceBuilder Resource) DotnetToolResource { + if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithMcpServer configures an MCP server endpoint on the resource func (s *dotnetToolResource) WithMcpServer(options ...*WithMcpServerOptions) DotnetToolResource { if s.err != nil { return s } @@ -10540,6 +10787,18 @@ func (s *dotnetToolResource) WithOtlpExporter(options ...*WithOtlpExporterOption return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *dotnetToolResource) WithParentProcessLifetime(parentProcessId float64) DotnetToolResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *dotnetToolResource) WithParentRelationship(parent Resource) DotnetToolResource { if s.err != nil { return s } @@ -10553,6 +10812,17 @@ func (s *dotnetToolResource) WithParentRelationship(parent Resource) DotnetToolR return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *dotnetToolResource) WithPersistentLifetime() DotnetToolResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *dotnetToolResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) DotnetToolResource { if s.err != nil { return s } @@ -10735,6 +11005,17 @@ func (s *dotnetToolResource) WithRequiredCommand(command string, options ...*Wit return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *dotnetToolResource) WithSessionLifetime() DotnetToolResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *dotnetToolResource) WithStatus(status TestResourceStatus) DotnetToolResource { if s.err != nil { return s } @@ -11995,6 +12276,7 @@ type ExecutableResource interface { WithDockerfileBaseImage(options ...*WithDockerfileBaseImageOptions) ExecutableResource WithEndpoint(options ...*WithEndpointOptions) ExecutableResource WithEndpointCallback(endpointName string, callback func(obj EndpointUpdateContext), options ...*WithEndpointCallbackOptions) ExecutableResource + WithEndpointProxySupport(proxyEnabled bool) ExecutableResource WithEndpoints(endpoints []string) ExecutableResource WithEnvironment(name string, value any) ExecutableResource WithEnvironmentCallback(callback func(arg EnvironmentCallbackContext)) ExecutableResource @@ -12013,6 +12295,7 @@ type ExecutableResource interface { WithHttpsEndpointCallback(callback func(obj EndpointUpdateContext), options ...*WithHttpsEndpointCallbackOptions) ExecutableResource WithIconName(iconName string, options ...*WithIconNameOptions) ExecutableResource WithImagePushOptions(callback func(arg ContainerImagePushOptionsCallbackContext)) ExecutableResource + WithLifetimeOf(sourceBuilder Resource) ExecutableResource WithMcpServer(options ...*WithMcpServerOptions) ExecutableResource WithMergeEndpoint(endpointName string, port float64) ExecutableResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) ExecutableResource @@ -12027,7 +12310,9 @@ type ExecutableResource interface { WithOptionalCallback(options ...*WithOptionalCallbackOptions) ExecutableResource WithOptionalString(options ...*WithOptionalStringOptions) ExecutableResource WithOtlpExporter(options ...*WithOtlpExporterOptions) ExecutableResource + WithParentProcessLifetime(parentProcessId float64) ExecutableResource WithParentRelationship(parent Resource) ExecutableResource + WithPersistentLifetime() ExecutableResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ExecutableResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) ExecutableResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) ExecutableResource @@ -12038,6 +12323,7 @@ type ExecutableResource interface { WithRemoteImageName(remoteImageName string) ExecutableResource WithRemoteImageTag(remoteImageTag string) ExecutableResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) ExecutableResource + WithSessionLifetime() ExecutableResource WithStatus(status TestResourceStatus) ExecutableResource WithUnionDependency(dependency any) ExecutableResource WithUrl(url any, options ...*WithUrlOptions) ExecutableResource @@ -12604,6 +12890,18 @@ func (s *executableResource) WithEndpointCallback(endpointName string, callback return s } +// WithEndpointProxySupport configures endpoint proxy support +func (s *executableResource) WithEndpointProxySupport(proxyEnabled bool) ExecutableResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["proxyEnabled"] = serializeValue(proxyEnabled) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withEndpointProxySupport", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithEndpoints sets the endpoints func (s *executableResource) WithEndpoints(endpoints []string) ExecutableResource { if s.err != nil { return s } @@ -12914,6 +13212,19 @@ func (s *executableResource) WithImagePushOptions(callback func(arg ContainerIma return s } +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *executableResource) WithLifetimeOf(sourceBuilder Resource) ExecutableResource { + if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithMcpServer configures an MCP server endpoint on the resource func (s *executableResource) WithMcpServer(options ...*WithMcpServerOptions) ExecutableResource { if s.err != nil { return s } @@ -13140,6 +13451,18 @@ func (s *executableResource) WithOtlpExporter(options ...*WithOtlpExporterOption return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *executableResource) WithParentProcessLifetime(parentProcessId float64) ExecutableResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *executableResource) WithParentRelationship(parent Resource) ExecutableResource { if s.err != nil { return s } @@ -13153,6 +13476,17 @@ func (s *executableResource) WithParentRelationship(parent Resource) ExecutableR return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *executableResource) WithPersistentLifetime() ExecutableResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *executableResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ExecutableResource { if s.err != nil { return s } @@ -13335,6 +13669,17 @@ func (s *executableResource) WithRequiredCommand(command string, options ...*Wit return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *executableResource) WithSessionLifetime() ExecutableResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *executableResource) WithStatus(status TestResourceStatus) ExecutableResource { if s.err != nil { return s } @@ -13770,6 +14115,7 @@ type ExternalServiceResource interface { WithHealthCheck(key string) ExternalServiceResource WithHttpHealthCheck(options ...*WithHttpHealthCheckOptions) ExternalServiceResource WithIconName(iconName string, options ...*WithIconNameOptions) ExternalServiceResource + WithLifetimeOf(sourceBuilder Resource) ExternalServiceResource WithMergeEndpoint(endpointName string, port float64) ExternalServiceResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) ExternalServiceResource WithMergeLabel(label string) ExternalServiceResource @@ -13782,13 +14128,16 @@ type ExternalServiceResource interface { WithNestedConfig(config *TestNestedDto) ExternalServiceResource WithOptionalCallback(options ...*WithOptionalCallbackOptions) ExternalServiceResource WithOptionalString(options ...*WithOptionalStringOptions) ExternalServiceResource + WithParentProcessLifetime(parentProcessId float64) ExternalServiceResource WithParentRelationship(parent Resource) ExternalServiceResource + WithPersistentLifetime() ExternalServiceResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ExternalServiceResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) ExternalServiceResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) ExternalServiceResource WithProcessCommandFactory(commandName string, displayName string, createProcessSpec func(arg ExecuteCommandContext) *ProcessCommandSpecExportData, options ...*WithProcessCommandFactoryOptions) ExternalServiceResource WithRelationship(resourceBuilder Resource, type_ string) ExternalServiceResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) ExternalServiceResource + WithSessionLifetime() ExternalServiceResource WithStatus(status TestResourceStatus) ExternalServiceResource WithUnionDependency(dependency any) ExternalServiceResource WithUrl(url any, options ...*WithUrlOptions) ExternalServiceResource @@ -14164,6 +14513,19 @@ func (s *externalServiceResource) WithIconName(iconName string, options ...*With return s } +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *externalServiceResource) WithLifetimeOf(sourceBuilder Resource) ExternalServiceResource { + if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithMergeEndpoint configures a named endpoint func (s *externalServiceResource) WithMergeEndpoint(endpointName string, port float64) ExternalServiceResource { if s.err != nil { return s } @@ -14354,6 +14716,18 @@ func (s *externalServiceResource) WithOptionalString(options ...*WithOptionalStr return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *externalServiceResource) WithParentProcessLifetime(parentProcessId float64) ExternalServiceResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *externalServiceResource) WithParentRelationship(parent Resource) ExternalServiceResource { if s.err != nil { return s } @@ -14367,6 +14741,17 @@ func (s *externalServiceResource) WithParentRelationship(parent Resource) Extern return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *externalServiceResource) WithPersistentLifetime() ExternalServiceResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *externalServiceResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ExternalServiceResource { if s.err != nil { return s } @@ -14487,6 +14872,17 @@ func (s *externalServiceResource) WithRequiredCommand(command string, options .. return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *externalServiceResource) WithSessionLifetime() ExternalServiceResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *externalServiceResource) WithStatus(status TestResourceStatus) ExternalServiceResource { if s.err != nil { return s } @@ -15230,6 +15626,7 @@ type ParameterResource interface { WithExplicitStart() ParameterResource WithHealthCheck(key string) ParameterResource WithIconName(iconName string, options ...*WithIconNameOptions) ParameterResource + WithLifetimeOf(sourceBuilder Resource) ParameterResource WithMergeEndpoint(endpointName string, port float64) ParameterResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) ParameterResource WithMergeLabel(label string) ParameterResource @@ -15242,13 +15639,16 @@ type ParameterResource interface { WithNestedConfig(config *TestNestedDto) ParameterResource WithOptionalCallback(options ...*WithOptionalCallbackOptions) ParameterResource WithOptionalString(options ...*WithOptionalStringOptions) ParameterResource + WithParentProcessLifetime(parentProcessId float64) ParameterResource WithParentRelationship(parent Resource) ParameterResource + WithPersistentLifetime() ParameterResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ParameterResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) ParameterResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) ParameterResource WithProcessCommandFactory(commandName string, displayName string, createProcessSpec func(arg ExecuteCommandContext) *ProcessCommandSpecExportData, options ...*WithProcessCommandFactoryOptions) ParameterResource WithRelationship(resourceBuilder Resource, type_ string) ParameterResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) ParameterResource + WithSessionLifetime() ParameterResource WithStatus(status TestResourceStatus) ParameterResource WithUnionDependency(dependency any) ParameterResource WithUrl(url any, options ...*WithUrlOptions) ParameterResource @@ -15637,6 +16037,19 @@ func (s *parameterResource) WithIconName(iconName string, options ...*WithIconNa return s } +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *parameterResource) WithLifetimeOf(sourceBuilder Resource) ParameterResource { + if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithMergeEndpoint configures a named endpoint func (s *parameterResource) WithMergeEndpoint(endpointName string, port float64) ParameterResource { if s.err != nil { return s } @@ -15827,6 +16240,18 @@ func (s *parameterResource) WithOptionalString(options ...*WithOptionalStringOpt return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *parameterResource) WithParentProcessLifetime(parentProcessId float64) ParameterResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *parameterResource) WithParentRelationship(parent Resource) ParameterResource { if s.err != nil { return s } @@ -15840,6 +16265,17 @@ func (s *parameterResource) WithParentRelationship(parent Resource) ParameterRes return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *parameterResource) WithPersistentLifetime() ParameterResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *parameterResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ParameterResource { if s.err != nil { return s } @@ -15960,6 +16396,17 @@ func (s *parameterResource) WithRequiredCommand(command string, options ...*With return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *parameterResource) WithSessionLifetime() ParameterResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *parameterResource) WithStatus(status TestResourceStatus) ParameterResource { if s.err != nil { return s } @@ -16781,6 +17228,7 @@ type ProjectResource interface { WithDockerfileBaseImage(options ...*WithDockerfileBaseImageOptions) ProjectResource WithEndpoint(options ...*WithEndpointOptions) ProjectResource WithEndpointCallback(endpointName string, callback func(obj EndpointUpdateContext), options ...*WithEndpointCallbackOptions) ProjectResource + WithEndpointProxySupport(proxyEnabled bool) ProjectResource WithEndpoints(endpoints []string) ProjectResource WithEnvironment(name string, value any) ProjectResource WithEnvironmentCallback(callback func(arg EnvironmentCallbackContext)) ProjectResource @@ -16798,6 +17246,7 @@ type ProjectResource interface { WithHttpsEndpointCallback(callback func(obj EndpointUpdateContext), options ...*WithHttpsEndpointCallbackOptions) ProjectResource WithIconName(iconName string, options ...*WithIconNameOptions) ProjectResource WithImagePushOptions(callback func(arg ContainerImagePushOptionsCallbackContext)) ProjectResource + WithLifetimeOf(sourceBuilder Resource) ProjectResource WithMcpServer(options ...*WithMcpServerOptions) ProjectResource WithMergeEndpoint(endpointName string, port float64) ProjectResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) ProjectResource @@ -16812,7 +17261,9 @@ type ProjectResource interface { WithOptionalCallback(options ...*WithOptionalCallbackOptions) ProjectResource WithOptionalString(options ...*WithOptionalStringOptions) ProjectResource WithOtlpExporter(options ...*WithOtlpExporterOptions) ProjectResource + WithParentProcessLifetime(parentProcessId float64) ProjectResource WithParentRelationship(parent Resource) ProjectResource + WithPersistentLifetime() ProjectResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ProjectResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) ProjectResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) ProjectResource @@ -16824,6 +17275,7 @@ type ProjectResource interface { WithRemoteImageTag(remoteImageTag string) ProjectResource WithReplicas(replicas float64) ProjectResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) ProjectResource + WithSessionLifetime() ProjectResource WithStatus(status TestResourceStatus) ProjectResource WithUnionDependency(dependency any) ProjectResource WithUrl(url any, options ...*WithUrlOptions) ProjectResource @@ -17421,6 +17873,18 @@ func (s *projectResource) WithEndpointCallback(endpointName string, callback fun return s } +// WithEndpointProxySupport configures endpoint proxy support +func (s *projectResource) WithEndpointProxySupport(proxyEnabled bool) ProjectResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["proxyEnabled"] = serializeValue(proxyEnabled) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withEndpointProxySupport", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithEndpoints sets the endpoints func (s *projectResource) WithEndpoints(endpoints []string) ProjectResource { if s.err != nil { return s } @@ -17719,6 +18183,19 @@ func (s *projectResource) WithImagePushOptions(callback func(arg ContainerImageP return s } +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *projectResource) WithLifetimeOf(sourceBuilder Resource) ProjectResource { + if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithMcpServer configures an MCP server endpoint on the resource func (s *projectResource) WithMcpServer(options ...*WithMcpServerOptions) ProjectResource { if s.err != nil { return s } @@ -17945,6 +18422,18 @@ func (s *projectResource) WithOtlpExporter(options ...*WithOtlpExporterOptions) return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *projectResource) WithParentProcessLifetime(parentProcessId float64) ProjectResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *projectResource) WithParentRelationship(parent Resource) ProjectResource { if s.err != nil { return s } @@ -17958,6 +18447,17 @@ func (s *projectResource) WithParentRelationship(parent Resource) ProjectResourc return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *projectResource) WithPersistentLifetime() ProjectResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *projectResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ProjectResource { if s.err != nil { return s } @@ -18152,6 +18652,17 @@ func (s *projectResource) WithRequiredCommand(command string, options ...*WithRe return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *projectResource) WithSessionLifetime() ProjectResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *projectResource) WithStatus(status TestResourceStatus) ProjectResource { if s.err != nil { return s } @@ -19723,6 +20234,7 @@ type TestDatabaseResource interface { WithImageSHA256(sha256 string) TestDatabaseResource WithImageTag(tag string) TestDatabaseResource WithLifetime(lifetime ContainerLifetime) TestDatabaseResource + WithLifetimeOf(sourceBuilder Resource) TestDatabaseResource WithMcpServer(options ...*WithMcpServerOptions) TestDatabaseResource WithMergeEndpoint(endpointName string, port float64) TestDatabaseResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) TestDatabaseResource @@ -19737,7 +20249,9 @@ type TestDatabaseResource interface { WithOptionalCallback(options ...*WithOptionalCallbackOptions) TestDatabaseResource WithOptionalString(options ...*WithOptionalStringOptions) TestDatabaseResource WithOtlpExporter(options ...*WithOtlpExporterOptions) TestDatabaseResource + WithParentProcessLifetime(parentProcessId float64) TestDatabaseResource WithParentRelationship(parent Resource) TestDatabaseResource + WithPersistentLifetime() TestDatabaseResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) TestDatabaseResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) TestDatabaseResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) TestDatabaseResource @@ -19748,6 +20262,7 @@ type TestDatabaseResource interface { WithRemoteImageName(remoteImageName string) TestDatabaseResource WithRemoteImageTag(remoteImageTag string) TestDatabaseResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) TestDatabaseResource + WithSessionLifetime() TestDatabaseResource WithStatus(status TestResourceStatus) TestDatabaseResource WithUnionDependency(dependency any) TestDatabaseResource WithUrl(url any, options ...*WithUrlOptions) TestDatabaseResource @@ -20872,6 +21387,19 @@ func (s *testDatabaseResource) WithLifetime(lifetime ContainerLifetime) TestData return s } +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *testDatabaseResource) WithLifetimeOf(sourceBuilder Resource) TestDatabaseResource { + if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithMcpServer configures an MCP server endpoint on the resource func (s *testDatabaseResource) WithMcpServer(options ...*WithMcpServerOptions) TestDatabaseResource { if s.err != nil { return s } @@ -21098,6 +21626,18 @@ func (s *testDatabaseResource) WithOtlpExporter(options ...*WithOtlpExporterOpti return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *testDatabaseResource) WithParentProcessLifetime(parentProcessId float64) TestDatabaseResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *testDatabaseResource) WithParentRelationship(parent Resource) TestDatabaseResource { if s.err != nil { return s } @@ -21111,6 +21651,17 @@ func (s *testDatabaseResource) WithParentRelationship(parent Resource) TestDatab return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *testDatabaseResource) WithPersistentLifetime() TestDatabaseResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *testDatabaseResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) TestDatabaseResource { if s.err != nil { return s } @@ -21293,6 +21844,17 @@ func (s *testDatabaseResource) WithRequiredCommand(command string, options ...*W return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *testDatabaseResource) WithSessionLifetime() TestDatabaseResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *testDatabaseResource) WithStatus(status TestResourceStatus) TestDatabaseResource { if s.err != nil { return s } @@ -21686,6 +22248,7 @@ type TestRedisResource interface { WithImageSHA256(sha256 string) TestRedisResource WithImageTag(tag string) TestRedisResource WithLifetime(lifetime ContainerLifetime) TestRedisResource + WithLifetimeOf(sourceBuilder Resource) TestRedisResource WithMcpServer(options ...*WithMcpServerOptions) TestRedisResource WithMergeEndpoint(endpointName string, port float64) TestRedisResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) TestRedisResource @@ -21701,8 +22264,10 @@ type TestRedisResource interface { WithOptionalCallback(options ...*WithOptionalCallbackOptions) TestRedisResource WithOptionalString(options ...*WithOptionalStringOptions) TestRedisResource WithOtlpExporter(options ...*WithOtlpExporterOptions) TestRedisResource + WithParentProcessLifetime(parentProcessId float64) TestRedisResource WithParentRelationship(parent Resource) TestRedisResource WithPersistence(options ...*WithPersistenceOptions) TestRedisResource + WithPersistentLifetime() TestRedisResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) TestRedisResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) TestRedisResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) TestRedisResource @@ -21714,6 +22279,7 @@ type TestRedisResource interface { WithRemoteImageName(remoteImageName string) TestRedisResource WithRemoteImageTag(remoteImageTag string) TestRedisResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) TestRedisResource + WithSessionLifetime() TestRedisResource WithStatus(status TestResourceStatus) TestRedisResource WithUnionDependency(dependency any) TestRedisResource WithUrl(url any, options ...*WithUrlOptions) TestRedisResource @@ -23057,6 +23623,19 @@ func (s *testRedisResource) WithLifetime(lifetime ContainerLifetime) TestRedisRe return s } +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *testRedisResource) WithLifetimeOf(sourceBuilder Resource) TestRedisResource { + if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithMcpServer configures an MCP server endpoint on the resource func (s *testRedisResource) WithMcpServer(options ...*WithMcpServerOptions) TestRedisResource { if s.err != nil { return s } @@ -23302,6 +23881,18 @@ func (s *testRedisResource) WithOtlpExporter(options ...*WithOtlpExporterOptions return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *testRedisResource) WithParentProcessLifetime(parentProcessId float64) TestRedisResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *testRedisResource) WithParentRelationship(parent Resource) TestRedisResource { if s.err != nil { return s } @@ -23333,6 +23924,17 @@ func (s *testRedisResource) WithPersistence(options ...*WithPersistenceOptions) return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *testRedisResource) WithPersistentLifetime() TestRedisResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *testRedisResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) TestRedisResource { if s.err != nil { return s } @@ -23527,6 +24129,17 @@ func (s *testRedisResource) WithRequiredCommand(command string, options ...*With return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *testRedisResource) WithSessionLifetime() TestRedisResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *testRedisResource) WithStatus(status TestResourceStatus) TestRedisResource { if s.err != nil { return s } diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index d585dc36378..07011cada6e 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -1706,6 +1706,44 @@ public CSharpAppResource withRequiredCommand(String command, String helpLink) { return this; } + /** Sets session lifetime behavior for the resource */ + public CSharpAppResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public CSharpAppResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public CSharpAppResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public CSharpAppResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public CSharpAppResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + public CSharpAppResource withEnvironment(String name, String value) { return withEnvironment(name, AspireUnion.of(value)); } @@ -1995,6 +2033,15 @@ private CSharpAppResource withEndpointImpl(Double port, Double targetPort, Strin return this; } + /** Configures endpoint proxy support */ + public CSharpAppResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public CSharpAppResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -3885,6 +3932,44 @@ public ContainerRegistryResource withRequiredCommand(String command, String help return this; } + /** Sets session lifetime behavior for the resource */ + public ContainerRegistryResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public ContainerRegistryResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public ContainerRegistryResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public ContainerRegistryResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public ContainerRegistryResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + /** Customizes displayed URLs via callback */ public ContainerRegistryResource withUrls(AspireAction1 callback) { Map reqArgs = new HashMap<>(); @@ -4755,15 +4840,6 @@ private ContainerResource withContainerCertificatePathsImpl(String customCertifi return this; } - /** Configures endpoint proxy support */ - public ContainerResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - public ContainerResource withDockerfileBuilder(String contextPath, AspireAction1 callback) { return withDockerfileBuilder(contextPath, callback, null); } @@ -4886,6 +4962,44 @@ public ContainerResource withRequiredCommand(String command, String helpLink) { return this; } + /** Sets session lifetime behavior for the resource */ + public ContainerResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public ContainerResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public ContainerResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public ContainerResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public ContainerResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + public ContainerResource withEnvironment(String name, String value) { return withEnvironment(name, AspireUnion.of(value)); } @@ -5175,6 +5289,15 @@ private ContainerResource withEndpointImpl(Double port, Double targetPort, Strin return this; } + /** Configures endpoint proxy support */ + public ContainerResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public ContainerResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -6996,6 +7119,44 @@ public DotnetToolResource withRequiredCommand(String command, String helpLink) { return this; } + /** Sets session lifetime behavior for the resource */ + public DotnetToolResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public DotnetToolResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public DotnetToolResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public DotnetToolResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public DotnetToolResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + public DotnetToolResource withEnvironment(String name, String value) { return withEnvironment(name, AspireUnion.of(value)); } @@ -7285,6 +7446,15 @@ private DotnetToolResource withEndpointImpl(Double port, Double targetPort, Stri return this; } + /** Configures endpoint proxy support */ + public DotnetToolResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public DotnetToolResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -9053,6 +9223,44 @@ public ExecutableResource withRequiredCommand(String command, String helpLink) { return this; } + /** Sets session lifetime behavior for the resource */ + public ExecutableResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public ExecutableResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public ExecutableResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public ExecutableResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public ExecutableResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + public ExecutableResource withEnvironment(String name, String value) { return withEnvironment(name, AspireUnion.of(value)); } @@ -9342,6 +9550,15 @@ private ExecutableResource withEndpointImpl(Double port, Double targetPort, Stri return this; } + /** Configures endpoint proxy support */ + public ExecutableResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public ExecutableResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -10566,6 +10783,44 @@ public ExternalServiceResource withRequiredCommand(String command, String helpLi return this; } + /** Sets session lifetime behavior for the resource */ + public ExternalServiceResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public ExternalServiceResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public ExternalServiceResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public ExternalServiceResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public ExternalServiceResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + /** Customizes displayed URLs via callback */ public ExternalServiceResource withUrls(AspireAction1 callback) { Map reqArgs = new HashMap<>(); @@ -13625,6 +13880,44 @@ public ParameterResource withRequiredCommand(String command, String helpLink) { return this; } + /** Sets session lifetime behavior for the resource */ + public ParameterResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public ParameterResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public ParameterResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public ParameterResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public ParameterResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + /** Customizes displayed URLs via callback */ public ParameterResource withUrls(AspireAction1 callback) { Map reqArgs = new HashMap<>(); @@ -14943,6 +15236,44 @@ public ProjectResource withRequiredCommand(String command, String helpLink) { return this; } + /** Sets session lifetime behavior for the resource */ + public ProjectResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public ProjectResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public ProjectResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public ProjectResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public ProjectResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + public ProjectResource withEnvironment(String name, String value) { return withEnvironment(name, AspireUnion.of(value)); } @@ -15232,6 +15563,15 @@ private ProjectResource withEndpointImpl(Double port, Double targetPort, String return this; } + /** Configures endpoint proxy support */ + public ProjectResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public ProjectResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -17535,15 +17875,6 @@ private TestDatabaseResource withContainerCertificatePathsImpl(String customCert return this; } - /** Configures endpoint proxy support */ - public TestDatabaseResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - public TestDatabaseResource withDockerfileBuilder(String contextPath, AspireAction1 callback) { return withDockerfileBuilder(contextPath, callback, null); } @@ -17666,6 +17997,44 @@ public TestDatabaseResource withRequiredCommand(String command, String helpLink) return this; } + /** Sets session lifetime behavior for the resource */ + public TestDatabaseResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public TestDatabaseResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public TestDatabaseResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public TestDatabaseResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public TestDatabaseResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + public TestDatabaseResource withEnvironment(String name, String value) { return withEnvironment(name, AspireUnion.of(value)); } @@ -17955,6 +18324,15 @@ private TestDatabaseResource withEndpointImpl(Double port, Double targetPort, St return this; } + /** Configures endpoint proxy support */ + public TestDatabaseResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public TestDatabaseResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -19443,15 +19821,6 @@ private TestRedisResource withContainerCertificatePathsImpl(String customCertifi return this; } - /** Configures endpoint proxy support */ - public TestRedisResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - public TestRedisResource withDockerfileBuilder(String contextPath, AspireAction1 callback) { return withDockerfileBuilder(contextPath, callback, null); } @@ -19574,6 +19943,44 @@ public TestRedisResource withRequiredCommand(String command, String helpLink) { return this; } + /** Sets session lifetime behavior for the resource */ + public TestRedisResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public TestRedisResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public TestRedisResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public TestRedisResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public TestRedisResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + public TestRedisResource withEnvironment(String name, String value) { return withEnvironment(name, AspireUnion.of(value)); } @@ -19889,6 +20296,15 @@ private TestRedisResource withEndpointImpl(Double port, Double targetPort, Strin return this; } + /** Configures endpoint proxy support */ + public TestRedisResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public TestRedisResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -21444,15 +21860,6 @@ private TestVaultResource withContainerCertificatePathsImpl(String customCertifi return this; } - /** Configures endpoint proxy support */ - public TestVaultResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - public TestVaultResource withDockerfileBuilder(String contextPath, AspireAction1 callback) { return withDockerfileBuilder(contextPath, callback, null); } @@ -21575,6 +21982,44 @@ public TestVaultResource withRequiredCommand(String command, String helpLink) { return this; } + /** Sets session lifetime behavior for the resource */ + public TestVaultResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public TestVaultResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public TestVaultResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public TestVaultResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public TestVaultResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + public TestVaultResource withEnvironment(String name, String value) { return withEnvironment(name, AspireUnion.of(value)); } @@ -21864,6 +22309,15 @@ private TestVaultResource withEndpointImpl(Double port, Double targetPort, Strin return this; } + /** Configures endpoint proxy support */ + public TestVaultResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public TestVaultResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -23950,3 +24404,4 @@ public WithVolumeOptions isReadOnly(Boolean value) { .modules/WithPipelineStepFactoryOptions.java .modules/WithReferenceOptions.java .modules/WithVolumeOptions.java + diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index 8a6bd087adb..c705b211f5f 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -1,4 +1,4 @@ -# ------------------------------------------------------------- +# ------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See LICENSE in project root for information. # @@ -4461,16 +4461,16 @@ def is_external(self, value: bool) -> None: ) @_uncached_property - def is_proxied(self) -> bool: + def is_proxied(self) -> bool | None: """Gets the IsProxied property""" result = self._client.invoke_capability( 'Aspire.Hosting.ApplicationModel/EndpointUpdateContext.isProxied', {'context': self._handle} ) - return typing.cast(bool, result) + return typing.cast(bool | None, result) @is_proxied.setter - def is_proxied(self, value: bool) -> None: + def is_proxied(self, value: bool | None) -> None: """Sets the IsProxied property""" self._client.invoke_capability( 'Aspire.Hosting.ApplicationModel/EndpointUpdateContext.setIsProxied', @@ -6085,6 +6085,22 @@ def with_dockerfile_base_image(self, *, build_image: str | None = None, runtime_ def with_required_command(self, command: str, *, help_link: str | None = None) -> typing.Self: """Adds a required command dependency""" + @abc.abstractmethod + def with_session_lifetime(self) -> typing.Self: + """Sets session lifetime behavior for the resource""" + + @abc.abstractmethod + def with_persistent_lifetime(self) -> typing.Self: + """Sets persistent lifetime behavior for the resource""" + + @abc.abstractmethod + def with_lifetime_of(self, source_builder: AbstractResource) -> typing.Self: + """Sets resource lifetime behavior to match another resource""" + + @abc.abstractmethod + def with_parent_process_lifetime(self, parent_process_id: int) -> typing.Self: + """Sets persistent lifetime behavior tied to a parent process""" + @abc.abstractmethod def with_urls(self, callback: typing.Callable[[ResourceUrlsCallbackContext], None]) -> typing.Self: """Customizes displayed URLs via callback""" @@ -6346,15 +6362,19 @@ def with_https_endpoint_callback(self, callback: typing.Callable[[EndpointUpdate """Updates an HTTPS endpoint via callback""" @abc.abstractmethod - def with_endpoint(self, *, port: int | None = None, target_port: int | None = None, scheme: str | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True, is_external: bool | None = None, protocol: ProtocolType | None = None) -> typing.Self: + def with_endpoint(self, *, port: int | None = None, target_port: int | None = None, scheme: str | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None, is_external: bool | None = None, protocol: ProtocolType | None = None) -> typing.Self: """Adds a network endpoint""" @abc.abstractmethod - def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True) -> typing.Self: + def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: + """Configures endpoint proxy support""" + + @abc.abstractmethod + def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTP endpoint""" @abc.abstractmethod - def with_https_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True) -> typing.Self: + def with_https_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTPS endpoint""" @abc.abstractmethod @@ -6476,6 +6496,10 @@ class _BaseResourceKwargs(typing.TypedDict, total=False): container_registry: AbstractResource dockerfile_base_image: DockerfileBaseImageParameters | typing.Literal[True] required_command: str | tuple[str, str] + session_lifetime: typing.Literal[True] + persistent_lifetime: typing.Literal[True] + lifetime_of: AbstractResource + parent_process_lifetime: int urls: typing.Callable[[ResourceUrlsCallbackContext], None] url: str | ReferenceExpression | tuple[str | ReferenceExpression, str] url_for_endpoint: tuple[str, typing.Callable[[ResourceUrlAnnotation], None]] @@ -6566,6 +6590,48 @@ def with_required_command(self, command: str, *, help_link: str | None = None) - self._handle = self._wrap_builder(result) return self + def with_session_lifetime(self) -> typing.Self: + """Sets session lifetime behavior for the resource""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + result = self._client.invoke_capability( + 'Aspire.Hosting/withSessionLifetime', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + + def with_persistent_lifetime(self) -> typing.Self: + """Sets persistent lifetime behavior for the resource""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + result = self._client.invoke_capability( + 'Aspire.Hosting/withPersistentLifetime', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + + def with_lifetime_of(self, source_builder: AbstractResource) -> typing.Self: + """Sets resource lifetime behavior to match another resource""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['sourceBuilder'] = source_builder + result = self._client.invoke_capability( + 'Aspire.Hosting/withLifetimeOf', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + + def with_parent_process_lifetime(self, parent_process_id: int) -> typing.Self: + """Sets persistent lifetime behavior tied to a parent process""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['parentProcessId'] = parent_process_id + result = self._client.invoke_capability( + 'Aspire.Hosting/withParentProcessLifetime', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + def with_urls(self, callback: typing.Callable[[ResourceUrlsCallbackContext], None]) -> typing.Self: """Customizes displayed URLs via callback""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -7079,6 +7145,32 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withRequiredCommand', rpc_args)) else: raise TypeError("Invalid type for option 'required_command'. Expected: str or (str, str)") + if _session_lifetime := kwargs.pop("session_lifetime", None): + if _session_lifetime is True: + rpc_args: dict[str, typing.Any] = {"builder": handle} + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withSessionLifetime', rpc_args)) + else: + raise TypeError("Invalid type for option 'session_lifetime'. Expected: Literal[True]") + if _persistent_lifetime := kwargs.pop("persistent_lifetime", None): + if _persistent_lifetime is True: + rpc_args: dict[str, typing.Any] = {"builder": handle} + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withPersistentLifetime', rpc_args)) + else: + raise TypeError("Invalid type for option 'persistent_lifetime'. Expected: Literal[True]") + if _lifetime_of := kwargs.pop("lifetime_of", None): + if _validate_type(_lifetime_of, AbstractResource): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["sourceBuilder"] = typing.cast(AbstractResource, _lifetime_of) + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withLifetimeOf', rpc_args)) + else: + raise TypeError("Invalid type for option 'lifetime_of'. Expected: AbstractResource") + if _parent_process_lifetime := kwargs.pop("parent_process_lifetime", None): + if _validate_type(_parent_process_lifetime, int): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["parentProcessId"] = typing.cast(int, _parent_process_lifetime) + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withParentProcessLifetime', rpc_args)) + else: + raise TypeError("Invalid type for option 'parent_process_lifetime'. Expected: int") if _urls := kwargs.pop("urls", None): if _validate_type(_urls, typing.Callable[[ResourceUrlsCallbackContext], None]): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -7455,7 +7547,6 @@ class ContainerResourceKwargs(_BaseResourceKwargs, total=False): build_arg: tuple[str, str | ParameterResource] build_secret: tuple[str, ParameterResource] container_certificate_paths: ContainerCertificatePathsParameters | typing.Literal[True] - endpoint_proxy_support: bool dockerfile_builder: tuple[str, typing.Callable[[DockerfileBuilderCallbackContext], None]] | DockerfileBuilderParameters container_network_alias: str mcp_server: McpServerParameters | typing.Literal[True] @@ -7471,6 +7562,7 @@ class ContainerResourceKwargs(_BaseResourceKwargs, total=False): http_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpEndpointCallbackParameters https_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpsEndpointCallbackParameters endpoint: EndpointParameters | typing.Literal[True] + endpoint_proxy_support: bool http_endpoint: HttpEndpointParameters | typing.Literal[True] https_endpoint: HttpsEndpointParameters | typing.Literal[True] external_http_endpoints: typing.Literal[True] @@ -7680,17 +7772,6 @@ def with_container_certificate_paths(self, *, custom_certificates_destination: s self._handle = self._wrap_builder(result) return self - def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: - """Configures endpoint proxy support""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['proxyEnabled'] = proxy_enabled - result = self._client.invoke_capability( - 'Aspire.Hosting/withEndpointProxySupport', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - def with_dockerfile_builder(self, context_path: str, callback: typing.Callable[[DockerfileBuilderCallbackContext], None], *, stage: str | None = None) -> typing.Self: """Configures the resource to use a programmatically generated Dockerfile""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -7869,7 +7950,7 @@ def with_https_endpoint_callback(self, callback: typing.Callable[[EndpointUpdate self._handle = self._wrap_builder(result) return self - def with_endpoint(self, *, port: int | None = None, target_port: int | None = None, scheme: str | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True, is_external: bool | None = None, protocol: ProtocolType | None = None) -> typing.Self: + def with_endpoint(self, *, port: int | None = None, target_port: int | None = None, scheme: str | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None, is_external: bool | None = None, protocol: ProtocolType | None = None) -> typing.Self: """Adds a network endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} if port is not None: @@ -7895,7 +7976,18 @@ def with_endpoint(self, *, port: int | None = None, target_port: int | None = No self._handle = self._wrap_builder(result) return self - def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True) -> typing.Self: + def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: + """Configures endpoint proxy support""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['proxyEnabled'] = proxy_enabled + result = self._client.invoke_capability( + 'Aspire.Hosting/withEndpointProxySupport', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + + def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTP endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} if port is not None: @@ -7915,7 +8007,7 @@ def with_http_endpoint(self, *, port: int | None = None, target_port: int | None self._handle = self._wrap_builder(result) return self - def with_https_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True) -> typing.Self: + def with_https_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTPS endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} if port is not None: @@ -8325,13 +8417,6 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withContainerCertificatePaths', rpc_args)) else: raise TypeError("Invalid type for option 'container_certificate_paths'. Expected: ContainerCertificatePathsParameters or Literal[True]") - if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): - if _validate_type(_endpoint_proxy_support, bool): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) - else: - raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _dockerfile_builder := kwargs.pop("dockerfile_builder", None): if _validate_tuple_types(_dockerfile_builder, (str, typing.Callable[[DockerfileBuilderCallbackContext], None])): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -8487,6 +8572,13 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpoint', rpc_args)) else: raise TypeError("Invalid type for option 'endpoint'. Expected: EndpointParameters or Literal[True]") + if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): + if _validate_type(_endpoint_proxy_support, bool): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) + else: + raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _http_endpoint := kwargs.pop("http_endpoint", None): if _validate_dict_types(_http_endpoint, HttpEndpointParameters): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -8720,6 +8812,7 @@ class ProjectResourceKwargs(_BaseResourceKwargs, total=False): http_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpEndpointCallbackParameters https_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpsEndpointCallbackParameters endpoint: EndpointParameters | typing.Literal[True] + endpoint_proxy_support: bool http_endpoint: HttpEndpointParameters | typing.Literal[True] https_endpoint: HttpsEndpointParameters | typing.Literal[True] external_http_endpoints: typing.Literal[True] @@ -8925,7 +9018,7 @@ def with_https_endpoint_callback(self, callback: typing.Callable[[EndpointUpdate self._handle = self._wrap_builder(result) return self - def with_endpoint(self, *, port: int | None = None, target_port: int | None = None, scheme: str | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True, is_external: bool | None = None, protocol: ProtocolType | None = None) -> typing.Self: + def with_endpoint(self, *, port: int | None = None, target_port: int | None = None, scheme: str | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None, is_external: bool | None = None, protocol: ProtocolType | None = None) -> typing.Self: """Adds a network endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} if port is not None: @@ -8951,7 +9044,18 @@ def with_endpoint(self, *, port: int | None = None, target_port: int | None = No self._handle = self._wrap_builder(result) return self - def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True) -> typing.Self: + def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: + """Configures endpoint proxy support""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['proxyEnabled'] = proxy_enabled + result = self._client.invoke_capability( + 'Aspire.Hosting/withEndpointProxySupport', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + + def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTP endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} if port is not None: @@ -8971,7 +9075,7 @@ def with_http_endpoint(self, *, port: int | None = None, target_port: int | None self._handle = self._wrap_builder(result) return self - def with_https_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True) -> typing.Self: + def with_https_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTPS endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} if port is not None: @@ -9400,6 +9504,13 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpoint', rpc_args)) else: raise TypeError("Invalid type for option 'endpoint'. Expected: EndpointParameters or Literal[True]") + if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): + if _validate_type(_endpoint_proxy_support, bool): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) + else: + raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _http_endpoint := kwargs.pop("http_endpoint", None): if _validate_dict_types(_http_endpoint, HttpEndpointParameters): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -9642,6 +9753,7 @@ class ExecutableResourceKwargs(_BaseResourceKwargs, total=False): http_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpEndpointCallbackParameters https_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpsEndpointCallbackParameters endpoint: EndpointParameters | typing.Literal[True] + endpoint_proxy_support: bool http_endpoint: HttpEndpointParameters | typing.Literal[True] https_endpoint: HttpsEndpointParameters | typing.Literal[True] external_http_endpoints: typing.Literal[True] @@ -9846,7 +9958,7 @@ def with_https_endpoint_callback(self, callback: typing.Callable[[EndpointUpdate self._handle = self._wrap_builder(result) return self - def with_endpoint(self, *, port: int | None = None, target_port: int | None = None, scheme: str | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True, is_external: bool | None = None, protocol: ProtocolType | None = None) -> typing.Self: + def with_endpoint(self, *, port: int | None = None, target_port: int | None = None, scheme: str | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None, is_external: bool | None = None, protocol: ProtocolType | None = None) -> typing.Self: """Adds a network endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} if port is not None: @@ -9872,7 +9984,18 @@ def with_endpoint(self, *, port: int | None = None, target_port: int | None = No self._handle = self._wrap_builder(result) return self - def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True) -> typing.Self: + def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: + """Configures endpoint proxy support""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['proxyEnabled'] = proxy_enabled + result = self._client.invoke_capability( + 'Aspire.Hosting/withEndpointProxySupport', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + + def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTP endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} if port is not None: @@ -9892,7 +10015,7 @@ def with_http_endpoint(self, *, port: int | None = None, target_port: int | None self._handle = self._wrap_builder(result) return self - def with_https_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True) -> typing.Self: + def with_https_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTPS endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} if port is not None: @@ -10307,6 +10430,13 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpoint', rpc_args)) else: raise TypeError("Invalid type for option 'endpoint'. Expected: EndpointParameters or Literal[True]") + if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): + if _validate_type(_endpoint_proxy_support, bool): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) + else: + raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _http_endpoint := kwargs.pop("http_endpoint", None): if _validate_dict_types(_http_endpoint, HttpEndpointParameters): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -11221,3 +11351,4 @@ def create_builder( _register_handle_wrapper("Aspire.Hosting.CodeGeneration.Python.Tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests.TestTypes.TestDatabaseResource", TestDatabaseResource) _register_handle_wrapper("Aspire.Hosting.CodeGeneration.Python.Tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests.TestTypes.TestRedisResource", TestRedisResource) _register_handle_wrapper("Aspire.Hosting.CodeGeneration.Python.Tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests.TestTypes.TestVaultResource", TestVaultResource) + diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index f7be691028a..e05d15eb545 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -1,4 +1,4 @@ -//! aspire.rs - Capability-based Aspire SDK +//! aspire.rs - Capability-based Aspire SDK //! GENERATED CODE - DO NOT EDIT use std::collections::HashMap; @@ -1760,6 +1760,44 @@ impl CSharpAppResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Sets an environment variable pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); @@ -1914,6 +1952,16 @@ impl CSharpAppResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -3190,6 +3238,44 @@ impl ContainerRegistryResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Customizes displayed URLs via callback pub fn with_urls(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -3907,16 +3993,6 @@ impl ContainerResource { Ok(ContainerResource::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(ContainerResource::new(handle, self.client.clone())) - } - /// Configures the resource to use a programmatically generated Dockerfile pub fn with_dockerfile_builder(&self, context_path: &str, callback: impl Fn(Vec) -> Value + Send + Sync + 'static, stage: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -4006,6 +4082,44 @@ impl ContainerResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Sets an environment variable pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); @@ -4160,6 +4274,16 @@ impl ContainerResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -5704,6 +5828,44 @@ impl DotnetToolResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Sets an environment variable pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); @@ -5858,6 +6020,16 @@ impl DotnetToolResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -7411,6 +7583,44 @@ impl ExecutableResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Sets an environment variable pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); @@ -7565,6 +7775,16 @@ impl ExecutableResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -8518,6 +8738,44 @@ impl ExternalServiceResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Customizes displayed URLs via callback pub fn with_urls(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -11097,6 +11355,44 @@ impl ParameterResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Customizes displayed URLs via callback pub fn with_urls(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -12191,6 +12487,44 @@ impl ProjectResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Sets an environment variable pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); @@ -12345,6 +12679,16 @@ impl ProjectResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -14038,16 +14382,6 @@ impl TestDatabaseResource { Ok(ContainerResource::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(ContainerResource::new(handle, self.client.clone())) - } - /// Configures the resource to use a programmatically generated Dockerfile pub fn with_dockerfile_builder(&self, context_path: &str, callback: impl Fn(Vec) -> Value + Send + Sync + 'static, stage: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -14137,6 +14471,44 @@ impl TestDatabaseResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Sets an environment variable pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); @@ -14291,6 +14663,16 @@ impl TestDatabaseResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -15452,16 +15834,6 @@ impl TestRedisResource { Ok(ContainerResource::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(ContainerResource::new(handle, self.client.clone())) - } - /// Configures the resource to use a programmatically generated Dockerfile pub fn with_dockerfile_builder(&self, context_path: &str, callback: impl Fn(Vec) -> Value + Send + Sync + 'static, stage: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -15551,6 +15923,44 @@ impl TestRedisResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Sets an environment variable pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); @@ -15725,6 +16135,16 @@ impl TestRedisResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -16972,16 +17392,6 @@ impl TestVaultResource { Ok(ContainerResource::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(ContainerResource::new(handle, self.client.clone())) - } - /// Configures the resource to use a programmatically generated Dockerfile pub fn with_dockerfile_builder(&self, context_path: &str, callback: impl Fn(Vec) -> Value + Send + Sync + 'static, stage: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -17071,6 +17481,44 @@ impl TestVaultResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Sets an environment variable pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); @@ -17225,6 +17673,16 @@ impl TestVaultResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -18147,3 +18605,4 @@ pub fn create_builder(options: Option) -> Result Promise; - set: (value: boolean) => Promise; + get: () => Promise; + set: (value: boolean | null) => Promise; }; /** Gets or sets a value indicating whether the endpoint is excluded from the default reference set. */ excludeReferenceEndpoint: { @@ -3911,13 +3911,13 @@ class EndpointUpdateContextImpl implements EndpointUpdateContext { }; isProxied = { - get: async (): Promise => { - return await this._client.invokeCapability( + get: async (): Promise => { + return await this._client.invokeCapability( 'Aspire.Hosting.ApplicationModel/EndpointUpdateContext.isProxied', { context: this._handle } ); }, - set: async (value: boolean): Promise => { + set: async (value: boolean | null): Promise => { await this._client.invokeCapability( 'Aspire.Hosting.ApplicationModel/EndpointUpdateContext.setIsProxied', { context: this._handle, value } @@ -10642,6 +10642,56 @@ export interface ContainerRegistryResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ContainerRegistryResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ContainerRegistryResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ContainerRegistryResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ContainerRegistryResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ContainerRegistryResourcePromise; /** * Registers a callback to customize the URLs displayed for the resource. * @@ -10991,6 +11041,56 @@ export interface ContainerRegistryResourcePromise extends PromiseLike("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ContainerRegistryResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ContainerRegistryResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ContainerRegistryResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ContainerRegistryResourcePromise; /** * Registers a callback to customize the URLs displayed for the resource. * @@ -11394,6 +11494,109 @@ class ContainerRegistryResourceImpl extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new ContainerRegistryResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ContainerRegistryResourcePromise { + return new ContainerRegistryResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new ContainerRegistryResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ContainerRegistryResourcePromise { + return new ContainerRegistryResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new ContainerRegistryResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ContainerRegistryResourcePromise { + return new ContainerRegistryResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new ContainerRegistryResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ContainerRegistryResourcePromise { + return new ContainerRegistryResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withUrlsInternal(callback: (obj: ResourceUrlsCallbackContext) => Promise): Promise { const callbackId = registerCallback(async (objData: unknown) => { @@ -12390,6 +12593,22 @@ class ContainerRegistryResourcePromiseImpl implements ContainerRegistryResourceP return new ContainerRegistryResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): ContainerRegistryResourcePromise { + return new ContainerRegistryResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): ContainerRegistryResourcePromise { + return new ContainerRegistryResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): ContainerRegistryResourcePromise { + return new ContainerRegistryResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): ContainerRegistryResourcePromise { + return new ContainerRegistryResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withUrls(callback: (obj: ResourceUrlsCallbackContext) => Promise): ContainerRegistryResourcePromise { return new ContainerRegistryResourcePromiseImpl(this._promise.then(obj => obj.withUrls(callback)), this._client); } @@ -12656,14 +12875,16 @@ export interface ContainerResource { /** * Sets the lifetime behavior of the container resource. * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. * Marking a container resource to have a `Persistent` lifetime. * ``` * var builder = DistributedApplication.CreateBuilder(args); * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); + * .WithPersistentLifetime(); * builder.Build().Run(); * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. * @returns The `IResourceBuilder`1`. */ withLifetime(lifetime: ContainerLifetime): ContainerResourcePromise; @@ -12744,17 +12965,6 @@ export interface ContainerResource { * @returns The updated resource builder. */ withContainerCertificatePaths(options?: WithContainerCertificatePathsOptions): ContainerResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Builds the specified container image from a Dockerfile generated by a callback using the `DockerfileBuilder` API. * @@ -12843,6 +13053,56 @@ export interface ContainerResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ContainerResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ContainerResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ContainerResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ContainerResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ContainerResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ContainerResourcePromise; /** @@ -12895,6 +13155,17 @@ export interface ContainerResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ContainerResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -13514,14 +13785,16 @@ export interface ContainerResourcePromise extends PromiseLike /** * Sets the lifetime behavior of the container resource. * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. * Marking a container resource to have a `Persistent` lifetime. * ``` * var builder = DistributedApplication.CreateBuilder(args); * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); + * .WithPersistentLifetime(); * builder.Build().Run(); * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. * @returns The `IResourceBuilder`1`. */ withLifetime(lifetime: ContainerLifetime): ContainerResourcePromise; @@ -13602,17 +13875,6 @@ export interface ContainerResourcePromise extends PromiseLike * @returns The updated resource builder. */ withContainerCertificatePaths(options?: WithContainerCertificatePathsOptions): ContainerResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Builds the specified container image from a Dockerfile generated by a callback using the `DockerfileBuilder` API. * @@ -13701,6 +13963,56 @@ export interface ContainerResourcePromise extends PromiseLike * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ContainerResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ContainerResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ContainerResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ContainerResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ContainerResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ContainerResourcePromise; /** @@ -13753,6 +14065,17 @@ export interface ContainerResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ContainerResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -14500,14 +14823,16 @@ class ContainerResourceImpl extends ResourceBuilderBase /** * Sets the lifetime behavior of the container resource. * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. * Marking a container resource to have a `Persistent` lifetime. * ``` * var builder = DistributedApplication.CreateBuilder(args); * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); + * .WithPersistentLifetime(); * builder.Build().Run(); * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. * @returns The `IResourceBuilder`1`. */ withLifetime(lifetime: ContainerLifetime): ContainerResourcePromise { @@ -14694,30 +15019,6 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withContainerCertificatePathsInternal(customCertificatesDestination, defaultCertificateBundlePaths, defaultCertificateDirectoryPaths), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new ContainerResourceImpl(result, this._client); - } - - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withDockerfileBuilderInternal(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, stage?: string): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -14916,6 +15217,109 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withRequiredCommandInternal(command, helpLink), this._client); } + /** @internal */ + private async _withSessionLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new ContainerResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new ContainerResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new ContainerResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new ContainerResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): Promise { value = isPromiseLike(value) ? await value : value; @@ -15157,6 +15561,30 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new ContainerResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -16799,10 +17227,6 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withContainerCertificatePaths(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBuilder(contextPath, callback, options)), this._client); } @@ -16831,6 +17255,22 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } @@ -16871,6 +17311,10 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -17247,6 +17691,56 @@ export interface CSharpAppResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): CSharpAppResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): CSharpAppResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): CSharpAppResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): CSharpAppResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): CSharpAppResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): CSharpAppResourcePromise; /** @@ -17299,6 +17793,17 @@ export interface CSharpAppResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): CSharpAppResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -17929,6 +18434,56 @@ export interface CSharpAppResourcePromise extends PromiseLike * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): CSharpAppResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): CSharpAppResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): CSharpAppResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): CSharpAppResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): CSharpAppResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): CSharpAppResourcePromise; /** @@ -17981,6 +18536,17 @@ export interface CSharpAppResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): CSharpAppResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -18743,6 +19309,109 @@ class CSharpAppResourceImpl extends ResourceBuilderBase return new CSharpAppResourcePromiseImpl(this._withRequiredCommandInternal(command, helpLink), this._client); } + /** @internal */ + private async _withSessionLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new CSharpAppResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new CSharpAppResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new CSharpAppResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new CSharpAppResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): Promise { value = isPromiseLike(value) ? await value : value; @@ -18984,6 +19653,30 @@ class CSharpAppResourceImpl extends ResourceBuilderBase return new CSharpAppResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new CSharpAppResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -20583,6 +21276,22 @@ class CSharpAppResourcePromiseImpl implements CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } @@ -20623,6 +21332,10 @@ class CSharpAppResourcePromiseImpl implements CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -21007,6 +21720,56 @@ export interface DotnetToolResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): DotnetToolResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): DotnetToolResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): DotnetToolResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): DotnetToolResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): DotnetToolResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): DotnetToolResourcePromise; /** @@ -21059,6 +21822,17 @@ export interface DotnetToolResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): DotnetToolResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -21691,6 +22465,56 @@ export interface DotnetToolResourcePromise extends PromiseLike("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): DotnetToolResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): DotnetToolResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): DotnetToolResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): DotnetToolResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): DotnetToolResourcePromise; /** @@ -21743,6 +22567,17 @@ export interface DotnetToolResourcePromise extends PromiseLike { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new DotnetToolResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new DotnetToolResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new DotnetToolResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new DotnetToolResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): Promise { value = isPromiseLike(value) ? await value : value; @@ -22824,6 +23762,30 @@ class DotnetToolResourceImpl extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new DotnetToolResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -24427,6 +25389,22 @@ class DotnetToolResourcePromiseImpl implements DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } @@ -24467,6 +25445,10 @@ class DotnetToolResourcePromiseImpl implements DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -24821,6 +25803,56 @@ export interface ExecutableResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ExecutableResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ExecutableResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ExecutableResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ExecutableResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ExecutableResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ExecutableResourcePromise; /** @@ -24873,6 +25905,17 @@ export interface ExecutableResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ExecutableResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -25472,6 +26515,56 @@ export interface ExecutableResourcePromise extends PromiseLike("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ExecutableResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ExecutableResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ExecutableResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ExecutableResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ExecutableResourcePromise; /** @@ -25524,6 +26617,17 @@ export interface ExecutableResourcePromise extends PromiseLike { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new ExecutableResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new ExecutableResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new ExecutableResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new ExecutableResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): Promise { value = isPromiseLike(value) ? await value : value; @@ -26501,6 +27708,30 @@ class ExecutableResourceImpl extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new ExecutableResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -28080,6 +29311,22 @@ class ExecutableResourcePromiseImpl implements ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } @@ -28120,6 +29367,10 @@ class ExecutableResourcePromiseImpl implements ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -28436,6 +29687,56 @@ export interface ExternalServiceResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ExternalServiceResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ExternalServiceResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ExternalServiceResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ExternalServiceResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ExternalServiceResourcePromise; /** * Registers a callback to customize the URLs displayed for the resource. * @@ -28790,6 +30091,56 @@ export interface ExternalServiceResourcePromise extends PromiseLike("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ExternalServiceResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ExternalServiceResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ExternalServiceResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ExternalServiceResourcePromise; /** * Registers a callback to customize the URLs displayed for the resource. * @@ -29217,6 +30568,109 @@ class ExternalServiceResourceImpl extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new ExternalServiceResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ExternalServiceResourcePromise { + return new ExternalServiceResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new ExternalServiceResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ExternalServiceResourcePromise { + return new ExternalServiceResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new ExternalServiceResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ExternalServiceResourcePromise { + return new ExternalServiceResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new ExternalServiceResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ExternalServiceResourcePromise { + return new ExternalServiceResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withUrlsInternal(callback: (obj: ResourceUrlsCallbackContext) => Promise): Promise { const callbackId = registerCallback(async (objData: unknown) => { @@ -30217,6 +31671,22 @@ class ExternalServiceResourcePromiseImpl implements ExternalServiceResourcePromi return new ExternalServiceResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): ExternalServiceResourcePromise { + return new ExternalServiceResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): ExternalServiceResourcePromise { + return new ExternalServiceResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): ExternalServiceResourcePromise { + return new ExternalServiceResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): ExternalServiceResourcePromise { + return new ExternalServiceResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withUrls(callback: (obj: ResourceUrlsCallbackContext) => Promise): ExternalServiceResourcePromise { return new ExternalServiceResourcePromiseImpl(this._promise.then(obj => obj.withUrls(callback)), this._client); } @@ -30454,6 +31924,56 @@ export interface ParameterResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ParameterResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ParameterResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ParameterResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ParameterResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ParameterResourcePromise; /** * Registers a callback to customize the URLs displayed for the resource. * @@ -30816,6 +32336,56 @@ export interface ParameterResourcePromise extends PromiseLike * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ParameterResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ParameterResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ParameterResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ParameterResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ParameterResourcePromise; /** * Registers a callback to customize the URLs displayed for the resource. * @@ -31261,6 +32831,109 @@ class ParameterResourceImpl extends ResourceBuilderBase return new ParameterResourcePromiseImpl(this._withRequiredCommandInternal(command, helpLink), this._client); } + /** @internal */ + private async _withSessionLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new ParameterResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ParameterResourcePromise { + return new ParameterResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new ParameterResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ParameterResourcePromise { + return new ParameterResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new ParameterResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ParameterResourcePromise { + return new ParameterResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new ParameterResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ParameterResourcePromise { + return new ParameterResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withUrlsInternal(callback: (obj: ResourceUrlsCallbackContext) => Promise): Promise { const callbackId = registerCallback(async (objData: unknown) => { @@ -32265,6 +33938,22 @@ class ParameterResourcePromiseImpl implements ParameterResourcePromise { return new ParameterResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): ParameterResourcePromise { + return new ParameterResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): ParameterResourcePromise { + return new ParameterResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): ParameterResourcePromise { + return new ParameterResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): ParameterResourcePromise { + return new ParameterResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withUrls(callback: (obj: ResourceUrlsCallbackContext) => Promise): ParameterResourcePromise { return new ParameterResourcePromiseImpl(this._promise.then(obj => obj.withUrls(callback)), this._client); } @@ -32550,6 +34239,56 @@ export interface ProjectResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ProjectResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ProjectResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ProjectResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ProjectResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ProjectResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ProjectResourcePromise; /** @@ -32602,6 +34341,17 @@ export interface ProjectResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ProjectResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -33232,6 +34982,56 @@ export interface ProjectResourcePromise extends PromiseLike { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ProjectResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ProjectResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ProjectResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ProjectResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ProjectResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ProjectResourcePromise; /** @@ -33284,6 +35084,17 @@ export interface ProjectResourcePromise extends PromiseLike { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ProjectResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -34047,6 +35858,109 @@ class ProjectResourceImpl extends ResourceBuilderBase imp return new ProjectResourcePromiseImpl(this._withRequiredCommandInternal(command, helpLink), this._client); } + /** @internal */ + private async _withSessionLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new ProjectResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new ProjectResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new ProjectResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new ProjectResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): Promise { value = isPromiseLike(value) ? await value : value; @@ -34288,6 +36202,30 @@ class ProjectResourceImpl extends ResourceBuilderBase imp return new ProjectResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new ProjectResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -35887,6 +37825,22 @@ class ProjectResourcePromiseImpl implements ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } @@ -35927,6 +37881,10 @@ class ProjectResourcePromiseImpl implements ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -36284,14 +38242,16 @@ export interface TestDatabaseResource { /** * Sets the lifetime behavior of the container resource. * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. * Marking a container resource to have a `Persistent` lifetime. * ``` * var builder = DistributedApplication.CreateBuilder(args); * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); + * .WithPersistentLifetime(); * builder.Build().Run(); * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. * @returns The `IResourceBuilder`1`. */ withLifetime(lifetime: ContainerLifetime): TestDatabaseResourcePromise; @@ -36372,17 +38332,6 @@ export interface TestDatabaseResource { * @returns The updated resource builder. */ withContainerCertificatePaths(options?: WithContainerCertificatePathsOptions): TestDatabaseResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise; /** * Builds the specified container image from a Dockerfile generated by a callback using the `DockerfileBuilder` API. * @@ -36471,6 +38420,56 @@ export interface TestDatabaseResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): TestDatabaseResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): TestDatabaseResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): TestDatabaseResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): TestDatabaseResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): TestDatabaseResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestDatabaseResourcePromise; /** @@ -36523,6 +38522,17 @@ export interface TestDatabaseResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestDatabaseResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -37142,14 +39152,16 @@ export interface TestDatabaseResourcePromise extends PromiseLike("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): TestDatabaseResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): TestDatabaseResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): TestDatabaseResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): TestDatabaseResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestDatabaseResourcePromise; /** @@ -37381,6 +39432,17 @@ export interface TestDatabaseResourcePromise extends PromiseLike { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new TestDatabaseResourceImpl(result, this._client); - } - - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withDockerfileBuilderInternal(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, stage?: string): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -38543,6 +40583,109 @@ class TestDatabaseResourceImpl extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new TestDatabaseResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new TestDatabaseResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new TestDatabaseResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new TestDatabaseResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): Promise { value = isPromiseLike(value) ? await value : value; @@ -38784,6 +40927,30 @@ class TestDatabaseResourceImpl extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new TestDatabaseResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -40426,10 +42593,6 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withContainerCertificatePaths(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBuilder(contextPath, callback, options)), this._client); } @@ -40458,6 +42621,22 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } @@ -40498,6 +42677,10 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -40855,14 +43038,16 @@ export interface TestRedisResource { /** * Sets the lifetime behavior of the container resource. * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. * Marking a container resource to have a `Persistent` lifetime. * ``` * var builder = DistributedApplication.CreateBuilder(args); * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); + * .WithPersistentLifetime(); * builder.Build().Run(); * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. * @returns The `IResourceBuilder`1`. */ withLifetime(lifetime: ContainerLifetime): TestRedisResourcePromise; @@ -40943,17 +43128,6 @@ export interface TestRedisResource { * @returns The updated resource builder. */ withContainerCertificatePaths(options?: WithContainerCertificatePathsOptions): TestRedisResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Builds the specified container image from a Dockerfile generated by a callback using the `DockerfileBuilder` API. * @@ -41042,6 +43216,56 @@ export interface TestRedisResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): TestRedisResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): TestRedisResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): TestRedisResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): TestRedisResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): TestRedisResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestRedisResourcePromise; /** @@ -41110,6 +43334,17 @@ export interface TestRedisResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestRedisResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -41777,14 +44012,16 @@ export interface TestRedisResourcePromise extends PromiseLike /** * Sets the lifetime behavior of the container resource. * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. * Marking a container resource to have a `Persistent` lifetime. * ``` * var builder = DistributedApplication.CreateBuilder(args); * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); + * .WithPersistentLifetime(); * builder.Build().Run(); * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. * @returns The `IResourceBuilder`1`. */ withLifetime(lifetime: ContainerLifetime): TestRedisResourcePromise; @@ -41865,17 +44102,6 @@ export interface TestRedisResourcePromise extends PromiseLike * @returns The updated resource builder. */ withContainerCertificatePaths(options?: WithContainerCertificatePathsOptions): TestRedisResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Builds the specified container image from a Dockerfile generated by a callback using the `DockerfileBuilder` API. * @@ -41964,6 +44190,56 @@ export interface TestRedisResourcePromise extends PromiseLike * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): TestRedisResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): TestRedisResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): TestRedisResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): TestRedisResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): TestRedisResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestRedisResourcePromise; /** @@ -42032,6 +44308,17 @@ export interface TestRedisResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestRedisResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -42826,14 +45113,16 @@ class TestRedisResourceImpl extends ResourceBuilderBase /** * Sets the lifetime behavior of the container resource. * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. * Marking a container resource to have a `Persistent` lifetime. * ``` * var builder = DistributedApplication.CreateBuilder(args); * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); + * .WithPersistentLifetime(); * builder.Build().Run(); * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. * @returns The `IResourceBuilder`1`. */ withLifetime(lifetime: ContainerLifetime): TestRedisResourcePromise { @@ -43020,30 +45309,6 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withContainerCertificatePathsInternal(customCertificatesDestination, defaultCertificateBundlePaths, defaultCertificateDirectoryPaths), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new TestRedisResourceImpl(result, this._client); - } - - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withDockerfileBuilderInternal(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, stage?: string): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -43242,6 +45507,109 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withRequiredCommandInternal(command, helpLink), this._client); } + /** @internal */ + private async _withSessionLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new TestRedisResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new TestRedisResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new TestRedisResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new TestRedisResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): Promise { value = isPromiseLike(value) ? await value : value; @@ -43519,6 +45887,30 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new TestRedisResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -45372,10 +47764,6 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withContainerCertificatePaths(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBuilder(contextPath, callback, options)), this._client); } @@ -45404,6 +47792,22 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } @@ -45452,6 +47856,10 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -45861,14 +48269,16 @@ export interface TestVaultResource { /** * Sets the lifetime behavior of the container resource. * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. * Marking a container resource to have a `Persistent` lifetime. * ``` * var builder = DistributedApplication.CreateBuilder(args); * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); + * .WithPersistentLifetime(); * builder.Build().Run(); * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. * @returns The `IResourceBuilder`1`. */ withLifetime(lifetime: ContainerLifetime): TestVaultResourcePromise; @@ -45949,17 +48359,6 @@ export interface TestVaultResource { * @returns The updated resource builder. */ withContainerCertificatePaths(options?: WithContainerCertificatePathsOptions): TestVaultResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Builds the specified container image from a Dockerfile generated by a callback using the `DockerfileBuilder` API. * @@ -46048,6 +48447,56 @@ export interface TestVaultResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): TestVaultResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): TestVaultResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): TestVaultResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): TestVaultResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): TestVaultResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestVaultResourcePromise; /** @@ -46100,6 +48549,17 @@ export interface TestVaultResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestVaultResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -46721,14 +49181,16 @@ export interface TestVaultResourcePromise extends PromiseLike /** * Sets the lifetime behavior of the container resource. * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. * Marking a container resource to have a `Persistent` lifetime. * ``` * var builder = DistributedApplication.CreateBuilder(args); * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); + * .WithPersistentLifetime(); * builder.Build().Run(); * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. * @returns The `IResourceBuilder`1`. */ withLifetime(lifetime: ContainerLifetime): TestVaultResourcePromise; @@ -46809,17 +49271,6 @@ export interface TestVaultResourcePromise extends PromiseLike * @returns The updated resource builder. */ withContainerCertificatePaths(options?: WithContainerCertificatePathsOptions): TestVaultResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Builds the specified container image from a Dockerfile generated by a callback using the `DockerfileBuilder` API. * @@ -46908,6 +49359,56 @@ export interface TestVaultResourcePromise extends PromiseLike * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): TestVaultResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): TestVaultResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): TestVaultResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): TestVaultResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): TestVaultResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestVaultResourcePromise; /** @@ -46960,6 +49461,17 @@ export interface TestVaultResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestVaultResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -47708,14 +50220,16 @@ class TestVaultResourceImpl extends ResourceBuilderBase /** * Sets the lifetime behavior of the container resource. * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. * Marking a container resource to have a `Persistent` lifetime. * ``` * var builder = DistributedApplication.CreateBuilder(args); * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); + * .WithPersistentLifetime(); * builder.Build().Run(); * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. * @returns The `IResourceBuilder`1`. */ withLifetime(lifetime: ContainerLifetime): TestVaultResourcePromise { @@ -47902,30 +50416,6 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withContainerCertificatePathsInternal(customCertificatesDestination, defaultCertificateBundlePaths, defaultCertificateDirectoryPaths), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new TestVaultResourceImpl(result, this._client); - } - - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withDockerfileBuilderInternal(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, stage?: string): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -48124,6 +50614,109 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withRequiredCommandInternal(command, helpLink), this._client); } + /** @internal */ + private async _withSessionLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new TestVaultResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new TestVaultResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new TestVaultResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new TestVaultResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): Promise { value = isPromiseLike(value) ? await value : value; @@ -48365,6 +50958,30 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new TestVaultResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -50022,10 +52639,6 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withContainerCertificatePaths(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBuilder(contextPath, callback, options)), this._client); } @@ -50054,6 +52667,22 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } @@ -50094,6 +52723,10 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -50745,6 +53378,56 @@ export interface Resource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ResourcePromise; /** * Registers a callback to customize the URLs displayed for the resource. * @@ -51094,6 +53777,56 @@ export interface ResourcePromise extends PromiseLike { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ResourcePromise; /** * Registers a callback to customize the URLs displayed for the resource. * @@ -51498,6 +54231,109 @@ class ResourceImpl extends ResourceBuilderBase implements Resou return new ResourcePromiseImpl(this._withRequiredCommandInternal(command, helpLink), this._client); } + /** @internal */ + private async _withSessionLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new ResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ResourcePromise { + return new ResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new ResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ResourcePromise { + return new ResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new ResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ResourcePromise { + return new ResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new ResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ResourcePromise { + return new ResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withUrlsInternal(callback: (obj: ResourceUrlsCallbackContext) => Promise): Promise { const callbackId = registerCallback(async (objData: unknown) => { @@ -52494,6 +55330,22 @@ class ResourcePromiseImpl implements ResourcePromise { return new ResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): ResourcePromise { + return new ResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): ResourcePromise { + return new ResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): ResourcePromise { + return new ResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): ResourcePromise { + return new ResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withUrls(callback: (obj: ResourceUrlsCallbackContext) => Promise): ResourcePromise { return new ResourcePromiseImpl(this._promise.then(obj => obj.withUrls(callback)), this._client); } @@ -53140,6 +55992,17 @@ export interface ResourceWithEndpoints { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ResourceWithEndpointsPromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -53244,6 +56107,17 @@ export interface ResourceWithEndpointsPromise extends PromiseLike { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new ResourceWithEndpointsImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise { + return new ResourceWithEndpointsPromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -53734,6 +56632,10 @@ class ResourceWithEndpointsPromiseImpl implements ResourceWithEndpointsPromise { return new ResourceWithEndpointsPromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise { + return new ResourceWithEndpointsPromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): ResourceWithEndpointsPromise { return new ResourceWithEndpointsPromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -54726,3 +57628,4 @@ registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceW registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment', (handle, client) => new ResourceWithEnvironmentImpl(handle as IResourceWithEnvironmentHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithWaitSupport', (handle, client) => new ResourceWithWaitSupportImpl(handle as IResourceWithWaitSupportHandle, client)); + diff --git a/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs b/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs index 7a66284836a..0066b97b898 100644 --- a/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs +++ b/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; @@ -55,6 +57,28 @@ public void AddDevTunnel_WithSpecificTunnelId_SetsTunnelIdProperty() Assert.Equal("custom-id", tunnel.Resource.TunnelId); } + [Fact] + public void AddDevTunnel_WithPersistentLifetime_AddsPersistenceAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var tunnel = builder.AddDevTunnel("tunnel", "custom-id") + .WithPersistentLifetime(); + + var annotation = Assert.Single(tunnel.Resource.Annotations.OfType()); + Assert.Equal(PersistenceMode.Persistent, annotation.Mode); + } + + [Fact] + public void AddDevTunnel_DefaultLifetimeDoesNotAddPersistenceAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var tunnel = builder.AddDevTunnel("tunnel", "custom-id"); + + Assert.Empty(tunnel.Resource.Annotations.OfType()); + } + [Fact] public void WithReference_WithAnonymousAccess_SetsPortAllowAnonymousOption() { @@ -70,6 +94,79 @@ public void WithReference_WithAnonymousAccess_SetsPortAllowAnonymousOption() Assert.True(port.Options.AllowAnonymous); } + [Fact] + public async Task WithReference_UsesTargetPortForDevTunnelPortWhenAvailable() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var target = builder.AddProject("target") + .WithHttpEndpoint(port: 5000, targetPort: 5001, name: "http"); + var tunnel = builder.AddDevTunnel("tunnel") + .WithReference(target); + + var tunnelPort = Assert.Single(tunnel.Resource.Ports); + + Assert.Equal(5001, await tunnelPort.GetTunnelPortAsync()); + } + + [Fact] + public async Task WithReference_UsesAllocatedPortForContainerDevTunnelPort() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var target = builder.AddContainer("target", "image") + .WithHttpEndpoint(port: 5000, targetPort: 8080, name: "http"); + var tunnel = builder.AddDevTunnel("tunnel") + .WithReference(target); + + var tunnelPort = Assert.Single(tunnel.Resource.Ports); + tunnelPort.TargetEndpoint.EndpointAnnotation.AllocatedEndpoint = new( + tunnelPort.TargetEndpoint.EndpointAnnotation, + "localhost", + 5000); + + Assert.Equal(5000, await tunnelPort.GetTunnelPortAsync()); + } + + [Fact] + public async Task WithReference_ResolvesDynamicTargetPortForDevTunnelPortWhenAvailable() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var target = builder.AddProject("target") + .WithHttpEndpoint(port: 5000, name: "http"); + var tunnel = builder.AddDevTunnel("tunnel") + .WithReference(target); + + var tunnelPort = Assert.Single(tunnel.Resource.Ports); + tunnelPort.TargetEndpoint.EndpointAnnotation.AllocatedEndpoint = new( + tunnelPort.TargetEndpoint.EndpointAnnotation, + "localhost", + 5000, + targetPortExpression: "5001"); + + Assert.Equal(5001, await tunnelPort.GetTunnelPortAsync()); + } + + [Fact] + public async Task WithReference_UsesAllocatedPortForDevTunnelPortWhenTargetPortIsUnavailable() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var target = builder.AddProject("target") + .WithHttpEndpoint(port: 5000, name: "http"); + var tunnel = builder.AddDevTunnel("tunnel") + .WithReference(target); + + var tunnelPort = Assert.Single(tunnel.Resource.Ports); + tunnelPort.TargetEndpoint.EndpointAnnotation.AllocatedEndpoint = new( + tunnelPort.TargetEndpoint.EndpointAnnotation, + "localhost", + 5000); + + Assert.Equal(5000, await tunnelPort.GetTunnelPortAsync()); + } + [Fact] public void GetEndpoint_WithResourceAndEndpointName_ReturnsTunnelEndpoint() { diff --git a/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs b/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs index 172e5720341..21b9d8385e2 100644 --- a/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs +++ b/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + using System.Data; using Aspire.TestUtilities; using Aspire.Hosting.ApplicationModel; @@ -572,14 +574,14 @@ public async Task MySql_WithPersistentLifetime_ReusesContainers(bool useMultiple var passwordParameter = builder.AddParameter("pwd", "p@ssw0rd1", secret: true); var mysql = builder - .AddMySql("resource", password: passwordParameter).WithLifetime(ContainerLifetime.Persistent) - .WithPhpMyAdmin(c => c.WithLifetime(ContainerLifetime.Persistent)) + .AddMySql("resource", password: passwordParameter).WithPersistentLifetime() + .WithPhpMyAdmin(c => c.WithPersistentLifetime()) .AddDatabase("db"); if (useMultipleInstances) { var passwordParameter2 = builder.AddParameter("pwd2", "p@ssw0rd2", secret: true); - builder.AddMySql("resource2", password: passwordParameter2).WithLifetime(ContainerLifetime.Persistent); + builder.AddMySql("resource2", password: passwordParameter2).WithPersistentLifetime(); } var app = builder.Build(); diff --git a/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs b/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs index 37dd7f65051..e4ac4040e8a 100644 --- a/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs +++ b/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + using System.Data; using System.Net; using Aspire.TestUtilities; @@ -559,9 +561,9 @@ public async Task Postgres_WithPersistentLifetime_ReusesContainers() var passwordParameter = builder.AddParameter("pwd", "p@ssword1", secret: true); builder - .AddPostgres("resource", password: passwordParameter).WithLifetime(ContainerLifetime.Persistent) - .WithPgWeb(c => c.WithLifetime(ContainerLifetime.Persistent)) - .WithPgAdmin(c => c.WithLifetime(ContainerLifetime.Persistent)) + .AddPostgres("resource", password: passwordParameter).WithPersistentLifetime() + .WithPgWeb(c => c.WithPersistentLifetime()) + .WithPgAdmin(c => c.WithPersistentLifetime()) .AddDatabase("mydb"); var app = builder.Build(); diff --git a/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj b/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj index ef4d47e9d71..e8e803885f9 100644 --- a/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj +++ b/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/tests/Aspire.Hosting.Tests/AspireStoreTests.cs b/tests/Aspire.Hosting.Tests/AspireStoreTests.cs index beb1ab00730..79982b5475e 100644 --- a/tests/Aspire.Hosting.Tests/AspireStoreTests.cs +++ b/tests/Aspire.Hosting.Tests/AspireStoreTests.cs @@ -53,6 +53,22 @@ public void BasePath_ShouldBePrefixed_WhenUsingConfiguration() Assert.Contains(".aspire", path); } + [Fact] + public void BasePath_ShouldFallbackToAppHostAspireDirectory_WhenIntermediateOutputMetadataIsUnavailable() + { + using var projectDirectory = new TestTempDirectory(); + var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions + { + AssemblyName = typeof(string).Assembly.GetName().Name, + ProjectDirectory = projectDirectory.Path + }); + + using var app = builder.Build(); + var store = app.Services.GetRequiredService(); + + Assert.Equal(Path.Combine(projectDirectory.Path, ".aspire"), store.BasePath); + } + [Fact] public void GetOrCreateFileWithContent_ShouldCreateFile_WithStreamContent() { diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index f0aea03a572..9acbc48c0a5 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -3,9 +3,12 @@ #pragma warning disable ASPIREEXTENSION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. using System.Collections.Concurrent; +using System.Diagnostics; using System.Globalization; using System.IO.Pipelines; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json; @@ -91,7 +94,7 @@ public async Task ResourceStarted_ProjectHasReplicas_EventRaisedOnce() var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, dcpOptions: dcpOptions, events: events); await appExecutor.RunApplicationAsync(); - var executables = kubernetesService.CreatedResources.OfType().ToList(); + var executables = GetCreatedExecutablesForResource(kubernetesService, "ServiceA"); Assert.Equal(2, executables.Count); var e = Assert.Single(startingEvents); @@ -159,7 +162,7 @@ public async Task CreateExecutable_LaunchProfileHasCommandLineArgs_AnnotationsAd var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, dcpOptions: dcpOptions, events: events, configuration: configuration); await appExecutor.RunApplicationAsync(); - var executables = kubernetesService.CreatedResources.OfType().ToList(); + var executables = GetCreatedExecutablesForResource(kubernetesService, "ServiceA"); var exe = Assert.Single(executables); // Ignore dotnet specific args for .NET project in process execution. @@ -230,7 +233,7 @@ public async Task CreateExecutable_ProjectArgsResolvedInSnapshot_UsesEffectiveAr var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetesService, "ServiceA"); Assert.True(exe.TryGetAnnotationAsObjectList(CustomResource.ResourceAppArgsAnnotation, out var argAnnotations)); Assert.Equal(2, argAnnotations.Count); AssertEffectiveArgumentIndexesMatchSpecArgs(argAnnotations, exe.Spec.Args); @@ -416,7 +419,7 @@ public async Task ResourceRestarted_EnvironmentCallbacksApplied() var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, dcpOptions: dcpOptions, events: events); await appExecutor.RunApplicationAsync(); - var executables = kubernetesService.CreatedResources.OfType().ToList(); + var executables = GetCreatedExecutablesForResource(kubernetesService, resource.Name); var exe1 = Assert.Single(executables); var callCount1 = exe1.Spec.Env!.Single(e => e.Name == "CALL_COUNT"); @@ -434,7 +437,7 @@ public async Task ResourceRestarted_EnvironmentCallbacksApplied() await appExecutor.StartResourceAsync(reference, CancellationToken.None); - executables = kubernetesService.CreatedResources.OfType().ToList(); + executables = GetCreatedExecutablesForResource(kubernetesService, resource.Name); Assert.Equal(2, executables.Count); var exe2 = executables[1]; @@ -486,7 +489,7 @@ public async Task EndpointPortsExecutableNotReplicatedProxiedPortSetNoTargetPort const int desiredPort = TestKubernetesService.StartOfAutoPortRange - 1000; var exe = builder.AddExecutable("CoolProgram", "cool", Environment.CurrentDirectory, "--alpha", "--bravo") - .WithEndpoint(name: "PortSetNoTargetPort", port: desiredPort, env: "PORT_SET_NO_TARGET_PORT", isProxied: true); + .WithEndpoint(name: "PortSetNoTargetPort", port: desiredPort, env: "PORT_SET_NO_TARGET_PORT"); var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); @@ -598,6 +601,99 @@ public async Task UnsupportedEndpointPortsExecutableNotReplicatedProxied() Assert.Contains("cannot be proxied when both TargetPort and Port are specified with the same value", exception.Message); } + [Fact] + public async Task EndpointPortsExecutableWithEndpointProxySupportUsesProxylessEndpoint() + { + var builder = DistributedApplication.CreateBuilder(); + + const int desiredPort = TestKubernetesService.StartOfAutoPortRange - 1001; + builder.AddExecutable("CoolProgram", "cool", Environment.CurrentDirectory, "--alpha", "--bravo") + .WithEndpoint(name: "PortSetNoTargetPort", port: desiredPort, env: "PORT_SET_NO_TARGET_PORT") + .WithEndpointProxySupport(false); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); + await appExecutor.RunApplicationAsync(); + + var dcpExe = Assert.Single(kubernetesService.CreatedResources.OfType()); + Assert.True(dcpExe.TryGetAnnotationAsObjectList(CustomResource.ServiceProducerAnnotation, out var spAnnList)); + + var svc = kubernetesService.CreatedResources.OfType().Single(s => s.Name() == "CoolProgram"); + Assert.Equal(AddressAllocationModes.Proxyless, svc.Spec.AddressAllocationMode); + Assert.Equal(desiredPort, svc.Status?.EffectivePort); + Assert.Equal(desiredPort, spAnnList.Single(ann => ann.ServiceName == "CoolProgram").Port); + + var envVarVal = dcpExe.Spec.Env?.Single(v => v.Name == "PORT_SET_NO_TARGET_PORT").Value; + Assert.False(string.IsNullOrWhiteSpace(envVarVal)); + Assert.Equal(desiredPort, int.Parse(envVarVal, CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task EndpointPortsExecutableWithEndpointProxySupportOverridesExplicitProxiedEndpoint() + { + var builder = DistributedApplication.CreateBuilder(); + + const int desiredPort = TestKubernetesService.StartOfAutoPortRange - 1001; + builder.AddExecutable("CoolProgram", "cool", Environment.CurrentDirectory, "--alpha", "--bravo") + .WithEndpoint(name: "EqualPortAndTargetPort", port: desiredPort, targetPort: desiredPort, env: "EQUAL_PORT_AND_TARGET_PORT", isProxied: true) + .WithEndpointProxySupport(false); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); + await appExecutor.RunApplicationAsync(); + + var dcpExe = Assert.Single(kubernetesService.CreatedResources.OfType()); + Assert.True(dcpExe.TryGetAnnotationAsObjectList(CustomResource.ServiceProducerAnnotation, out var spAnnList)); + + var svc = kubernetesService.CreatedResources.OfType().Single(s => s.Name() == "CoolProgram"); + Assert.Equal(AddressAllocationModes.Proxyless, svc.Spec.AddressAllocationMode); + Assert.Equal(desiredPort, svc.Status?.EffectivePort); + Assert.Equal(desiredPort, spAnnList.Single(ann => ann.ServiceName == "CoolProgram").Port); + + var envVarVal = dcpExe.Spec.Env?.Single(v => v.Name == "EQUAL_PORT_AND_TARGET_PORT").Value; + Assert.False(string.IsNullOrWhiteSpace(envVarVal)); + Assert.Equal(desiredPort, int.Parse(envVarVal, CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task EndpointPortsPersistentExecutableDefaultsToProxylessEndpoint() + { + var builder = DistributedApplication.CreateBuilder(); + + const int desiredPort = TestKubernetesService.StartOfAutoPortRange - 1002; + builder.AddExecutable("CoolProgram", "cool", Environment.CurrentDirectory, "--alpha", "--bravo") + .WithPersistentLifetime() + .WithEndpoint(name: "PortSetNoTargetPort", port: desiredPort, env: "PORT_SET_NO_TARGET_PORT"); + + var configDict = new Dictionary + { + ["AppHost:Sha256"] = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configDict).Build(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, configuration: configuration); + await appExecutor.RunApplicationAsync(); + + var dcpExe = Assert.Single(kubernetesService.CreatedResources.OfType()); + Assert.True(dcpExe.TryGetAnnotationAsObjectList(CustomResource.ServiceProducerAnnotation, out var spAnnList)); + + var svc = kubernetesService.CreatedResources.OfType().Single(s => s.Name() == "CoolProgram"); + Assert.Equal(AddressAllocationModes.Proxyless, svc.Spec.AddressAllocationMode); + Assert.Equal(desiredPort, svc.Status?.EffectivePort); + Assert.Equal(desiredPort, spAnnList.Single(ann => ann.ServiceName == "CoolProgram").Port); + + var envVarVal = dcpExe.Spec.Env?.Single(v => v.Name == "PORT_SET_NO_TARGET_PORT").Value; + Assert.False(string.IsNullOrWhiteSpace(envVarVal)); + Assert.Equal(desiredPort, int.Parse(envVarVal, CultureInfo.InvariantCulture)); + } + [Fact] public async Task EndpointPortsExecutableNotReplicatedProxylessPortSetNoTargetPort() { @@ -607,9 +703,6 @@ public async Task EndpointPortsExecutableNotReplicatedProxylessPortSetNoTargetPo builder.AddExecutable("CoolProgram", "cool", Environment.CurrentDirectory, "--alpha", "--bravo") .WithEndpoint(name: "PortSetNoTargetPort", port: desiredPort, env: "PORT_SET_NO_TARGET_PORT", isProxied: false); - // All these configurations are effectively the same because EndpointAnnotation constructor for proxy-less endpoints - // will make sure Port and TargetPort have the same value if one is specified but the other is not. - var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); var distributedAppModel = app.Services.GetRequiredService(); @@ -641,9 +734,6 @@ public async Task EndpointPortsExecutableNotReplicatedProxylessNoPortTargetPortS builder.AddExecutable("CoolProgram", "cool", Environment.CurrentDirectory, "--alpha", "--bravo") .WithEndpoint(name: "NoPortTargetPortSet", targetPort: desiredPort, env: "NO_PORT_TARGET_PORT_SET", isProxied: false); - // All these configurations are effectively the same because EndpointAnnotation constructor for proxy-less endpoints - // will make sure Port and TargetPort have the same value if one is specified but the other is not. - var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); var distributedAppModel = app.Services.GetRequiredService(); @@ -675,9 +765,6 @@ public async Task EndpointPortsExecutableNotReplicatedProxylessPortAndTargetPort builder.AddExecutable("CoolProgram", "cool", Environment.CurrentDirectory, "--alpha", "--bravo") .WithEndpoint(name: "PortAndTargetPortSet", port: desiredPort, targetPort: desiredPort, env: "PORT_AND_TARGET_PORT_SET", isProxied: false); - // All these configurations are effectively the same because EndpointAnnotation constructor for proxy-less endpoints - // will make sure Port and TargetPort have the same value if one is specified but the other is not. - var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); var distributedAppModel = app.Services.GetRequiredService(); @@ -771,7 +858,7 @@ public async Task EndpointOtelServiceName(int replicaCount, string expectedName) var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, dcpOptions: dcpOptions); await appExecutor.RunApplicationAsync(); - var executables = kubernetesService.CreatedResources.OfType().ToList(); + var executables = GetCreatedExecutablesForResource(kubernetesService, "ServiceA"); Assert.Equal(replicaCount, executables.Count); foreach (var exe in executables) @@ -1209,7 +1296,7 @@ public async Task EndpointPortsProjectNoPortNoTargetPort() var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); await appExecutor.RunApplicationAsync(); - var exes = kubernetesService.CreatedResources.OfType().ToList(); + var exes = GetCreatedExecutablesForResource(kubernetesService, "ServiceA"); Assert.Equal(3, exes.Count); foreach (var dcpExe in exes) @@ -1245,7 +1332,7 @@ public async Task EndpointPortsProjectPortSetNoTargetPort() const int desiredPortOne = TestKubernetesService.StartOfAutoPortRange - 1000; builder.AddProject("ServiceA") - .WithEndpoint(name: "PortSetNoTargetPort", port: desiredPortOne, env: "PORT_SET_NO_TARGET_PORT", isProxied: true) + .WithEndpoint(name: "PortSetNoTargetPort", port: desiredPortOne, env: "PORT_SET_NO_TARGET_PORT") .WithReplicas(3); var kubernetesService = new TestKubernetesService(); @@ -1254,7 +1341,7 @@ public async Task EndpointPortsProjectPortSetNoTargetPort() var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); await appExecutor.RunApplicationAsync(); - var exes = kubernetesService.CreatedResources.OfType().ToList(); + var exes = GetCreatedExecutablesForResource(kubernetesService, "ServiceA"); Assert.Equal(3, exes.Count); foreach (var dcpExe in exes) @@ -1276,6 +1363,74 @@ public async Task EndpointPortsProjectPortSetNoTargetPort() } } + [Fact] + public async Task EndpointPortsProjectWithEndpointProxySupportUsesProxylessEndpoint() + { + var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions + { + AssemblyName = typeof(DistributedApplicationTests).Assembly.FullName + }); + + const int desiredPort = TestKubernetesService.StartOfAutoPortRange - 1001; + builder.AddProject("ServiceA", launchProfileName: null) + .WithHttpEndpoint(name: "stable", port: desiredPort) + .WithEndpointProxySupport(false); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); + await appExecutor.RunApplicationAsync(); + + var dcpExe = GetCreatedExecutableForResource(kubernetesService, "ServiceA"); + Assert.True(dcpExe.TryGetAnnotationAsObjectList(CustomResource.ServiceProducerAnnotation, out var spAnnList)); + + var svc = kubernetesService.CreatedResources.OfType().Single(s => s.Name() == "ServiceA"); + Assert.Equal(AddressAllocationModes.Proxyless, svc.Spec.AddressAllocationMode); + Assert.Equal(desiredPort, svc.Status?.EffectivePort); + Assert.Equal(desiredPort, spAnnList.Single(ann => ann.ServiceName == "ServiceA").Port); + + var aspnetCoreUrls = dcpExe.Spec.Env?.Single(v => v.Name == "ASPNETCORE_URLS").Value; + Assert.Equal($"http://localhost:{desiredPort}", aspnetCoreUrls); + } + + [Fact] + public async Task EndpointPortsPersistentProjectDefaultsToProxylessEndpoint() + { + var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions + { + AssemblyName = typeof(DistributedApplicationTests).Assembly.FullName + }); + + const int desiredPort = TestKubernetesService.StartOfAutoPortRange - 1002; + builder.AddProject("ServiceA", launchProfileName: null) + .WithPersistentLifetime() + .WithHttpEndpoint(name: "stable", port: desiredPort); + + var configDict = new Dictionary + { + ["AppHost:Sha256"] = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configDict).Build(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, configuration: configuration); + await appExecutor.RunApplicationAsync(); + + var dcpExe = GetCreatedExecutableForResource(kubernetesService, "ServiceA"); + Assert.True(dcpExe.TryGetAnnotationAsObjectList(CustomResource.ServiceProducerAnnotation, out var spAnnList)); + + var svc = kubernetesService.CreatedResources.OfType().Single(s => s.Name() == "ServiceA"); + Assert.Equal(AddressAllocationModes.Proxyless, svc.Spec.AddressAllocationMode); + Assert.Equal(desiredPort, svc.Status?.EffectivePort); + Assert.Equal(desiredPort, spAnnList.Single(ann => ann.ServiceName == "ServiceA").Port); + + var aspnetCoreUrls = dcpExe.Spec.Env?.Single(v => v.Name == "ASPNETCORE_URLS").Value; + Assert.Equal($"http://localhost:{desiredPort}", aspnetCoreUrls); + } + [Fact] public async Task EndpointPortsConainerProxiedNoPortTargetPortSet() { @@ -1398,9 +1553,6 @@ public async Task EndpointPortsContainerProxylessPortSetNoTargetPort() builder.AddContainer("database", "image") .WithEndpoint(name: "PortSetNoTargetPort", port: desiredPort, env: "PORT_SET_NO_TARGET_PORT", isProxied: false); - // All these configurations are effectively the same because EndpointAnnotation constructor for proxy-less endpoints - // will make sure Port and TargetPort have the same value if one is specified but the other is not. - var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); var distributedAppModel = app.Services.GetRequiredService(); @@ -1434,9 +1586,6 @@ public async Task EndpointPortsContainerProxylessNoPortTargetPortSet() builder.AddContainer("database", "image") .WithEndpoint(name: "NoPortTargetPortSet", targetPort: desiredTargetPort, env: "NO_PORT_TARGET_PORT_SET", isProxied: false); - // All these configurations are effectively the same because EndpointAnnotation constructor for proxy-less endpoints - // will make sure Port and TargetPort have the same value if one is specified but the other is not. - var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); var distributedAppModel = app.Services.GetRequiredService(); @@ -1446,14 +1595,15 @@ public async Task EndpointPortsContainerProxylessNoPortTargetPortSet() var dcpCtr = Assert.Single(kubernetesService.CreatedResources.OfType()); Assert.True(dcpCtr.TryGetAnnotationAsObjectList(CustomResource.ServiceProducerAnnotation, out var spAnnList)); - // Port is empty, TargetPort is set + // Port is empty, TargetPort is set. // Clients connect directly to the container host port, MAY have the container host port injected. - // Container is using TargetPort for BOTH listening inside the container and as a host port. + // DCP allocates the container host port after the container is created. var svc = kubernetesService.CreatedResources.OfType().Single(s => s.Name() == "database"); Assert.Equal(AddressAllocationModes.Proxyless, svc.Spec.AddressAllocationMode); - Assert.Equal(desiredTargetPort, svc.Status?.EffectivePort); + Assert.Null(svc.Spec.Port); + Assert.True(svc.Status?.EffectivePort >= TestKubernetesService.StartOfAutoPortRange); Assert.NotNull(dcpCtr.Spec.Ports); - Assert.Contains(dcpCtr.Spec.Ports!, p => p.HostPort == desiredTargetPort && p.ContainerPort == desiredTargetPort); + Assert.Contains(dcpCtr.Spec.Ports!, p => p.HostPort is null && p.ContainerPort == desiredTargetPort); // Desired port should be part of the service producer annotation. Assert.Equal(desiredTargetPort, spAnnList.Single(ann => ann.ServiceName == "database").Port); var envVarVal = dcpCtr.Spec.Env?.Single(v => v.Name == "NO_PORT_TARGET_PORT_SET").Value; @@ -1461,6 +1611,77 @@ public async Task EndpointPortsContainerProxylessNoPortTargetPortSet() Assert.Equal(desiredTargetPort, int.Parse(envVarVal, CultureInfo.InvariantCulture)); } + [Fact] + public async Task EndpointPortsContainerProxylessNoPortTargetPortSetPublishesAllocatedEndpointAfterServiceUpdate() + { + var builder = DistributedApplication.CreateBuilder(); + + const int desiredTargetPort = TestKubernetesService.StartOfAutoPortRange - 999; + builder.AddContainer("database", "image") + .WithEndpoint(name: "NoPortTargetPortSet", targetPort: desiredTargetPort, isProxied: false); + + var allocatedPortChannel = Channel.CreateUnbounded(); + var eventing = new Hosting.Eventing.DistributedApplicationEventing(); + eventing.Subscribe((@event, ct) => + { + if (@event.Resource.Name == "database") + { + var endpoint = ((IResourceWithEndpoints)@event.Resource).GetEndpoint("NoPortTargetPortSet"); + if (endpoint.AllocatedEndpoint is { } allocatedEndpoint) + { + allocatedPortChannel.Writer.TryWrite(allocatedEndpoint.Port); + } + } + + return Task.CompletedTask; + }); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, distributedApplicationEventing: eventing); + await appExecutor.RunApplicationAsync(); + + var allocatedPort = await allocatedPortChannel.Reader.ReadAsync().AsTask().DefaultTimeout(); + + Assert.True(allocatedPort >= TestKubernetesService.StartOfAutoPortRange); + } + + [Fact] + public async Task ResourceEndpointsAllocatedEventSubscribersBlockDcpStartup() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddContainer("database", "image") + .WithHttpEndpoint(targetPort: 8080); + + var subscriberEntered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var releaseSubscriber = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var eventing = new Hosting.Eventing.DistributedApplicationEventing(); + eventing.Subscribe(async (@event, ct) => + { + if (@event.Resource.Name == "database") + { + subscriberEntered.TrySetResult(); + await releaseSubscriber.Task.WaitAsync(ct).ConfigureAwait(false); + } + }); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, distributedApplicationEventing: eventing); + + var runTask = appExecutor.RunApplicationAsync(); + await subscriberEntered.Task.DefaultTimeout(); + + var startupWasBlocked = !runTask.IsCompleted; + releaseSubscriber.SetResult(); + await runTask.DefaultTimeout(); + + Assert.True(startupWasBlocked); + } + [Fact] public async Task EndpointPortsContainerProxylessPortAndTargetPortSet() { @@ -1471,9 +1692,6 @@ public async Task EndpointPortsContainerProxylessPortAndTargetPortSet() builder.AddContainer("database", "image") .WithEndpoint(name: "PortAndTargetPortSet", port: desiredPort, targetPort: desiredTargetPort, env: "PORT_AND_TARGET_PORT_SET", isProxied: false); - // All these configurations are effectively the same because EndpointAnnotation constructor for proxy-less endpoints - // will make sure Port and TargetPort have the same value if one is specified but the other is not. - var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); var distributedAppModel = app.Services.GetRequiredService(); @@ -1498,6 +1716,38 @@ public async Task EndpointPortsContainerProxylessPortAndTargetPortSet() Assert.Equal(desiredTargetPort, int.Parse(envVarVal, CultureInfo.InvariantCulture)); } + [Fact] + public async Task EndpointPortsContainerWithEndpointProxySupportOverridesExplicitProxiedEndpoint() + { + var builder = DistributedApplication.CreateBuilder(); + + const int desiredPort = TestKubernetesService.StartOfAutoPortRange - 998; + const int desiredTargetPort = TestKubernetesService.StartOfAutoPortRange - 997; + builder.AddContainer("database", "image") + .WithEndpoint(name: "PortAndTargetPortSet", port: desiredPort, targetPort: desiredTargetPort, env: "PORT_AND_TARGET_PORT_SET", isProxied: true) + .WithEndpointProxySupport(false); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); + await appExecutor.RunApplicationAsync(); + + var dcpCtr = Assert.Single(kubernetesService.CreatedResources.OfType()); + Assert.True(dcpCtr.TryGetAnnotationAsObjectList(CustomResource.ServiceProducerAnnotation, out var spAnnList)); + + var svc = kubernetesService.CreatedResources.OfType().Single(s => s.Name() == "database"); + Assert.Equal(AddressAllocationModes.Proxyless, svc.Spec.AddressAllocationMode); + Assert.Equal(desiredPort, svc.Status?.EffectivePort); + Assert.NotNull(dcpCtr.Spec.Ports); + Assert.Contains(dcpCtr.Spec.Ports!, p => p.HostPort == desiredPort && p.ContainerPort == desiredTargetPort); + Assert.Equal(desiredTargetPort, spAnnList.Single(ann => ann.ServiceName == "database").Port); + + var envVarVal = dcpCtr.Spec.Env?.Single(v => v.Name == "PORT_AND_TARGET_PORT_SET").Value; + Assert.False(string.IsNullOrWhiteSpace(envVarVal)); + Assert.Equal(desiredTargetPort, int.Parse(envVarVal, CultureInfo.InvariantCulture)); + } + [Fact] public async Task EndpointPortsContainerProxylessProtocolSet() { @@ -1508,9 +1758,6 @@ public async Task EndpointPortsContainerProxylessProtocolSet() builder.AddContainer("database", "image") .WithEndpoint(name: "PortAndProtocolSet", port: desiredPort, targetPort: desiredTargetPort, env: "PORT_AND_PROTOCOL_SET", isProxied: false, protocol: System.Net.Sockets.ProtocolType.Udp); - // All these configurations are effectively the same because EndpointAnnotation constructor for proxy-less endpoints - // will make sure Port and TargetPort have the same value if one is specified but the other is not. - var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); var distributedAppModel = app.Services.GetRequiredService(); @@ -1693,7 +1940,7 @@ public async Task ProjectLaunchConfiguration_Populated_WhenLaunchProfileSpecifie await executor.RunApplicationAsync(); // Assert - var exe = Assert.Single(kubernetes.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetes, "proj"); Assert.True(exe.TryGetProjectLaunchConfiguration(out var plc)); Assert.NotNull(plc); Assert.False(plc!.DisableLaunchProfile); @@ -1727,7 +1974,7 @@ public async Task ProjectLaunchConfiguration_RespectsDebugSessionRunMode(string await executor.RunApplicationAsync(); - var exe = Assert.Single(kubernetes.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetes, "proj"); Assert.True(exe.TryGetProjectLaunchConfiguration(out var plc)); Assert.NotNull(plc); Assert.Equal(expectedMode, plc!.Mode); @@ -1761,7 +2008,7 @@ public async Task ProjectLaunchConfiguration_Disabled_WhenLaunchProfileExcluded_ await executor.RunApplicationAsync(); // Assert - var exe = Assert.Single(kubernetes.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetes, "proj"); Assert.True(exe.TryGetProjectLaunchConfiguration(out var plc)); Assert.NotNull(plc); Assert.True(plc!.DisableLaunchProfile); @@ -1797,7 +2044,7 @@ public async Task ProjectLaunchConfiguration_DefaultLaunchProfileAnnotationFalls await executor.RunApplicationAsync(); // Assert - var exe = Assert.Single(kubernetes.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetes, "proj"); Assert.True(exe.TryGetProjectLaunchConfiguration(out var plc)); Assert.NotNull(plc); // Should have fallen back to the first available profile (in insertion order) which is Foo, not the missing one. @@ -1833,7 +2080,7 @@ public async Task ProjectLaunchConfiguration_DefaultLaunchProfileAnnotationSelec var executor = CreateAppExecutor(model, configuration: configuration, kubernetesService: kubernetes); await executor.RunApplicationAsync(); - var exe = Assert.Single(kubernetes.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetes, "proj"); Assert.True(exe.TryGetProjectLaunchConfiguration(out var plc)); Assert.False(plc!.DisableLaunchProfile); Assert.Equal("http", plc.LaunchProfile); @@ -1864,7 +2111,7 @@ public async Task ProjectLaunchConfiguration_ExplicitLaunchProfileOverridesDefau var executor = CreateAppExecutor(model, configuration: configuration, kubernetesService: kubernetes); await executor.RunApplicationAsync(); - var exe = Assert.Single(kubernetes.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetes, "proj"); Assert.True(exe.TryGetProjectLaunchConfiguration(out var plc)); Assert.False(plc!.DisableLaunchProfile); Assert.Equal("http", plc.LaunchProfile); // explicit wins @@ -1895,7 +2142,7 @@ public async Task ProjectLaunchConfiguration_DefaultIgnoredWhenExcluded_InDebugS var executor = CreateAppExecutor(model, configuration: configuration, kubernetesService: kubernetes); await executor.RunApplicationAsync(); - var exe = Assert.Single(kubernetes.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetes, "proj"); Assert.True(exe.TryGetProjectLaunchConfiguration(out var plc)); Assert.True(plc!.DisableLaunchProfile); Assert.Equal(string.Empty, plc.LaunchProfile); @@ -1925,7 +2172,7 @@ public async Task ProjectLaunchConfiguration_NoProfiles_NoLaunchProfileSelected_ var executor = CreateAppExecutor(model, configuration: configuration, kubernetesService: kubernetes); await executor.RunApplicationAsync(); - var exe = Assert.Single(kubernetes.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetes, "proj"); Assert.True(exe.TryGetProjectLaunchConfiguration(out var plc)); Assert.False(plc!.DisableLaunchProfile); // not excluded Assert.Equal(string.Empty, plc.LaunchProfile); // nothing selected @@ -1954,7 +2201,7 @@ public async Task ProjectLaunchConfiguration_FallbackToFirstProfileInsertionOrde var executor = CreateAppExecutor(model, configuration: configuration, kubernetesService: kubernetes); await executor.RunApplicationAsync(); - var exe = Assert.Single(kubernetes.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetes, "proj"); Assert.True(exe.TryGetProjectLaunchConfiguration(out var plc)); Assert.False(plc!.DisableLaunchProfile); Assert.Equal("Zed", plc.LaunchProfile); // first inserted wins @@ -2015,6 +2262,300 @@ public async Task PlainExecutable_ExtensionMode_SupportedDebugMode_RunsInIde() Assert.False(nonDebuggableExe.TryGetAnnotationAsObjectList(Executable.LaunchConfigurationsAnnotation, out _)); } + [Fact] + public async Task PersistentPlainExecutable_ExtensionMode_RunsInProcess() + { + var builder = DistributedApplication.CreateBuilder(); + + var executable = new TestExecutableResource("test-working-directory"); + builder.AddResource(executable) + .WithDebugSupport(mode => new ExecutableLaunchConfiguration("test") { Mode = mode }, "test") + .WithPersistentLifetime(); + + var configDict = new Dictionary + { + ["AppHost:Sha256"] = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + [DcpExecutor.DebugSessionPortVar] = "12345", + [KnownConfigNames.DebugSessionInfo] = JsonSerializer.Serialize(new RunSessionInfo { ProtocolsSupported = ["test"], SupportedLaunchConfigurations = ["test"] }), + [KnownConfigNames.ExtensionEndpoint] = "http://localhost:1234", + [KnownConfigNames.DebugSessionRunMode] = "Debug" + }; + + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configDict).Build(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, configuration: configuration); + + await appExecutor.RunApplicationAsync(); + + var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "TestExecutable"); + Assert.Equal("TestExecutable-12345678", exe.Metadata.Name); + Assert.True(exe.Spec.Persistent.GetValueOrDefault()); + Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); + Assert.Null(exe.Spec.FallbackExecutionTypes); + } + + [Fact] + public async Task PersistentDcpResourcesDoNotIncludeMonitorProcessByDefault() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddContainer("database", "image") + .WithPersistentLifetime(); + builder.AddExecutable("worker", "worker", Environment.CurrentDirectory) + .WithPersistentLifetime(); + builder.AddProject("project", launchProfileName: null) + .WithPersistentLifetime(); + + var configDict = new Dictionary + { + ["AppHost:Sha256"] = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configDict).Build(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor( + distributedAppModel, + kubernetesService: kubernetesService, + configuration: configuration); + + await appExecutor.RunApplicationAsync(); + + var container = Assert.Single(kubernetesService.CreatedResources.OfType()); + Assert.True(container.Spec.Persistent.GetValueOrDefault()); + Assert.Null(container.Spec.MonitorPid); + Assert.Null(container.Spec.MonitorTimestamp); + + var executables = kubernetesService.CreatedResources.OfType() + .Where(e => e.AppModelResourceName is "worker" or "project") + .ToArray(); + Assert.Equal(2, executables.Length); + Assert.All(executables, exe => + { + Assert.True(exe.Spec.Persistent.GetValueOrDefault()); + Assert.Null(exe.Spec.MonitorPid); + Assert.Null(exe.Spec.MonitorTimestamp); + Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); + }); + } + + [Fact] + public async Task PersistentProjectWithReplicasThrows() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddProject("project", launchProfileName: null) + .WithReplicas(2) + .WithPersistentLifetime(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); + + var exception = await Assert.ThrowsAsync(() => appExecutor.RunApplicationAsync()); + Assert.Equal("Resource 'project' uses multiple replicas and a persistent lifetime. These features do not work together.", exception.Message); + } + + [Fact] + public async Task PersistentPlainExecutableWithReplicasThrows() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddExecutable("worker", "worker", Environment.CurrentDirectory) + .WithAnnotation(new ReplicaAnnotation(2)) + .WithPersistentLifetime(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); + + var exception = await Assert.ThrowsAsync(() => appExecutor.RunApplicationAsync()); + Assert.Equal("Resource 'worker' uses multiple replicas and a persistent lifetime. These features do not work together.", exception.Message); + } + + [Fact] + public async Task PersistentContainerWithOtlpExporterUsesStableServiceInstanceId() + { + var first = await CreateOtlpServiceInstanceIdAsync(builder => + { + builder.AddContainer("database", "image") + .WithPersistentLifetime() + .WithOtlpExporter(); + }); + var second = await CreateOtlpServiceInstanceIdAsync(builder => + { + builder.AddContainer("database", "image") + .WithPersistentLifetime() + .WithOtlpExporter(); + }); + + Assert.Equal("database-12345678", first); + Assert.Equal(first, second); + } + + [Fact] + public async Task PersistentExecutableWithOtlpExporterUsesStableServiceInstanceId() + { + var first = await CreateOtlpServiceInstanceIdAsync(builder => + { + builder.AddExecutable("worker", "worker", Environment.CurrentDirectory) + .WithPersistentLifetime() + .WithOtlpExporter(); + }); + var second = await CreateOtlpServiceInstanceIdAsync(builder => + { + builder.AddExecutable("worker", "worker", Environment.CurrentDirectory) + .WithPersistentLifetime() + .WithOtlpExporter(); + }); + + Assert.Equal("worker-12345678", first); + Assert.Equal(first, second); + } + + private static async Task CreateOtlpServiceInstanceIdAsync(Action configureBuilder) + { + var builder = DistributedApplication.CreateBuilder(); + configureBuilder(builder); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AppHost:Sha256"] = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + }) + .Build(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor( + distributedAppModel, + kubernetesService: kubernetesService, + configuration: configuration); + + await appExecutor.RunApplicationAsync(); + + var resource = Assert.Single(kubernetesService.CreatedResources, r => + r.Metadata.Annotations is not null && + r.Metadata.Annotations.ContainsKey(CustomResource.OtelServiceInstanceIdAnnotation)); + + return resource.Metadata.Annotations![CustomResource.OtelServiceInstanceIdAnnotation]; + } + + [Fact] + public async Task ExplicitParentProcessLifetimeIncludesMonitorProcess() + { + var builder = DistributedApplication.CreateBuilder(); + using var parentProcess = Process.GetCurrentProcess(); + var parentProcessIdentity = DcpProcessMonitor.GetMonitorProcessIdentity(parentProcess); + + builder.AddContainer("database", "image") + .WithParentProcessLifetime(parentProcess.Id); + builder.AddExecutable("worker", "worker", Environment.CurrentDirectory) + .WithParentProcessLifetime(parentProcess.Id); + builder.AddProject("project", launchProfileName: null) + .WithParentProcessLifetime(parentProcess.Id); + + var configDict = new Dictionary + { + ["AppHost:Sha256"] = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configDict).Build(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor( + distributedAppModel, + kubernetesService: kubernetesService, + configuration: configuration); + + await appExecutor.RunApplicationAsync(); + + var container = Assert.Single(kubernetesService.CreatedResources.OfType()); + Assert.True(container.Spec.Persistent.GetValueOrDefault()); + Assert.Equal(parentProcessIdentity.ProcessId, container.Spec.MonitorPid); + Assert.Equal(parentProcessIdentity.Timestamp, container.Spec.MonitorTimestamp); + + var executables = kubernetesService.CreatedResources.OfType() + .Where(e => e.AppModelResourceName is "worker" or "project") + .ToArray(); + Assert.Equal(2, executables.Length); + Assert.All(executables, exe => + { + Assert.True(exe.Spec.Persistent.GetValueOrDefault()); + Assert.Equal(parentProcessIdentity.ProcessId, exe.Spec.MonitorPid); + Assert.Equal(parentProcessIdentity.Timestamp, exe.Spec.MonitorTimestamp); + Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); + }); + } + + [Fact] + public async Task PersistentPlainExecutable_UsesStableCertificateOutputPath() + { + var builder = DistributedApplication.CreateBuilder(); + using var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + using var aspireStoreDirectory = fileSystemService.TempDirectory.CreateTempSubdirectory("aspire-store"); + + using var certificate = CreateTestCertificate(); + var certificateAuthorities = builder.AddCertificateAuthorityCollection("certificates") + .WithCertificate(certificate); + + var executable = new TestExecutableResource("test-working-directory"); + builder.AddResource(executable) + .WithCertificateAuthorityCollection(certificateAuthorities) + .WithCertificateTrustScope(CertificateTrustScope.Override) + .WithPersistentLifetime(); + + var configDict = new Dictionary + { + [AspireStore.AspireStorePathKeyName] = aspireStoreDirectory.Path, + ["AppHost:Sha256"] = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + }; + + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configDict).Build(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, configuration: configuration); + + await appExecutor.RunApplicationAsync(); + + var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "TestExecutable"); + var sslCertDir = Assert.Single(exe.Spec.Env!, e => e.Name == "SSL_CERT_DIR").Value; + var sslCertFile = Assert.Single(exe.Spec.Env!, e => e.Name == "SSL_CERT_FILE").Value; + var expectedCertificatesRoot = Path.Join(aspireStoreDirectory.Path, ".aspire", "dcp", "executables", "TestExecutable-12345678", "certificates"); + + Assert.Equal(Path.Join(expectedCertificatesRoot, "certs"), sslCertDir); + Assert.Equal(Path.Join(expectedCertificatesRoot, "cert.pem"), sslCertFile); + } + + [Fact] + public async Task ExplicitStartPlainExecutable_IsCreatedWithStartFalse() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddExecutable("CoolProgram", "cool", Environment.CurrentDirectory, "--alpha", "--bravo") + .WithExplicitStart(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); + + await appExecutor.RunApplicationAsync(); + + var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "CoolProgram"); + Assert.False(exe.Spec.Start.GetValueOrDefault(true)); + } + [Fact] public async Task PlainExecutable_ExtensionMode_UnsupportedDebugMode_RunsInProcess() { @@ -2320,11 +2861,7 @@ public async Task ProjectExecutable_NoDebugSessionInfo_DefaultsToProjectSupport( // Act await appExecutor.RunApplicationAsync(); - // Assert - var dcpExes = kubernetesService.CreatedResources.OfType().ToList(); - Assert.Single(dcpExes); - - var exe = Assert.Single(dcpExes, e => e.AppModelResourceName == "ServiceA"); + var exe = GetCreatedExecutableForResource(kubernetesService, "ServiceA"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); } @@ -2357,11 +2894,7 @@ public async Task ProjectExecutable_InvalidDebugSessionInfo_DefaultsToProjectSup // Act await appExecutor.RunApplicationAsync(); - // Assert - var dcpExes = kubernetesService.CreatedResources.OfType().ToList(); - Assert.Single(dcpExes); - - var exe = Assert.Single(dcpExes, e => e.AppModelResourceName == "ServiceA"); + var exe = GetCreatedExecutableForResource(kubernetesService, "ServiceA"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); } @@ -2400,11 +2933,7 @@ public async Task ProjectExecutable_DebugSessionInfoWithNullSupportedLaunchConfi // Act await appExecutor.RunApplicationAsync(); - // Assert - var dcpExes = kubernetesService.CreatedResources.OfType().ToList(); - Assert.Single(dcpExes); - - var exe = Assert.Single(dcpExes, e => e.AppModelResourceName == "ServiceA"); + var exe = GetCreatedExecutableForResource(kubernetesService, "ServiceA"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); } @@ -2445,7 +2974,7 @@ public async Task ProjectExecutable_DebugSessionInfoWithoutProjectFallsBackToPro await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "ServiceA"); + var exe = GetCreatedExecutableForResource(kubernetesService, "ServiceA"); Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); } @@ -2480,7 +3009,7 @@ public async Task ProjectWithNonProjectAnnotation_DebugSessionWithoutInfo_FallsB await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "proj"); + var exe = GetCreatedExecutableForResource(kubernetesService, "proj"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); Assert.NotNull(exe.Spec.FallbackExecutionTypes); Assert.Equal(ExecutionType.Process, Assert.Single(exe.Spec.FallbackExecutionTypes)); @@ -2528,7 +3057,7 @@ public async Task ProjectWithNonProjectAnnotation_VSCodeExplicitlyUnsupported_Ru await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "proj"); + var exe = GetCreatedExecutableForResource(kubernetesService, "proj"); Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); } @@ -2554,7 +3083,7 @@ public async Task ProjectWithNonProjectAnnotation_NoDebugSession_RunsInProcess() await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "proj"); + var exe = GetCreatedExecutableForResource(kubernetesService, "proj"); Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); } @@ -2596,7 +3125,7 @@ public async Task ProjectWithNonProjectAnnotation_VSCodeWithMatchingSupport_Runs await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "proj"); + var exe = GetCreatedExecutableForResource(kubernetesService, "proj"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); } @@ -2635,13 +3164,10 @@ public async Task StandardAndCustomProjects_VSScenario_BothRunInIde() await appExecutor.RunApplicationAsync(); - var dcpExes = kubernetesService.CreatedResources.OfType().ToList(); - Assert.Equal(2, dcpExes.Count); - - var standardExe = Assert.Single(dcpExes, e => e.AppModelResourceName == "standard-project"); + var standardExe = GetCreatedExecutableForResource(kubernetesService, "standard-project"); Assert.Equal(ExecutionType.IDE, standardExe.Spec.ExecutionType); - var customExe = Assert.Single(dcpExes, e => e.AppModelResourceName == "custom-project"); + var customExe = GetCreatedExecutableForResource(kubernetesService, "custom-project"); Assert.Equal(ExecutionType.IDE, customExe.Spec.ExecutionType); Assert.NotNull(customExe.Spec.FallbackExecutionTypes); Assert.Equal(ExecutionType.Process, Assert.Single(customExe.Spec.FallbackExecutionTypes)); @@ -2696,15 +3222,12 @@ public async Task StandardAndCustomProjects_VSCodeScenario_BothRunInIde() await appExecutor.RunApplicationAsync(); - var dcpExes = kubernetesService.CreatedResources.OfType().ToList(); - Assert.Equal(2, dcpExes.Count); - // Standard project: Process execution because the IDE did not advertise "project" support. - var standardExe = Assert.Single(dcpExes, e => e.AppModelResourceName == "standard-project"); + var standardExe = GetCreatedExecutableForResource(kubernetesService, "standard-project"); Assert.Equal(ExecutionType.Process, standardExe.Spec.ExecutionType); // Azure Functions project: IDE via explicit "azure-functions" support. - var functionsExe = Assert.Single(dcpExes, e => e.AppModelResourceName == "functions-project"); + var functionsExe = GetCreatedExecutableForResource(kubernetesService, "functions-project"); Assert.Equal(ExecutionType.IDE, functionsExe.Spec.ExecutionType); } @@ -2737,7 +3260,7 @@ public async Task ProjectWithNonProjectAnnotation_VSFallback_HasProcessFallbackE await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "proj"); + var exe = GetCreatedExecutableForResource(kubernetesService, "proj"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); Assert.NotNull(exe.Spec.FallbackExecutionTypes); Assert.Single(exe.Spec.FallbackExecutionTypes); @@ -2819,7 +3342,7 @@ public async Task ProjectExecutable_NoSupportsDebuggingAnnotation_InDebugSession await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "ServiceA"); + var exe = GetCreatedExecutableForResource(kubernetesService, "ServiceA"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); Assert.NotNull(exe.Spec.FallbackExecutionTypes); Assert.Equal(ExecutionType.Process, Assert.Single(exe.Spec.FallbackExecutionTypes)); @@ -2853,7 +3376,7 @@ public async Task ProjectExecutable_NoSupportsDebuggingAnnotation_NoDebugSession await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "ServiceA"); + var exe = GetCreatedExecutableForResource(kubernetesService, "ServiceA"); Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); } @@ -2888,7 +3411,7 @@ public async Task ProjectExecutable_NoAnnotation_ExecutableLaunchProfile_InDebug await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "TestFunction"); + var exe = GetCreatedExecutableForResource(kubernetesService, "TestFunction"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); Assert.NotNull(exe.Spec.FallbackExecutionTypes); Assert.Equal(ExecutionType.Process, Assert.Single(exe.Spec.FallbackExecutionTypes)); @@ -2931,7 +3454,7 @@ public async Task ProjectExecutable_NoAnnotation_ProjectLaunchProfile_InDebugSes await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "proj"); + var exe = GetCreatedExecutableForResource(kubernetesService, "proj"); // Should be IDE, because it's a normal Project profile Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); Assert.NotNull(exe.Spec.FallbackExecutionTypes); @@ -3626,16 +4149,7 @@ public async Task Project_NonProjectLaunchConfig_ExtensionMode_RunsInIde() await appExecutor.RunApplicationAsync(); // Assert - List dcpExes = []; - var haveExes = RetryTillTrueOrTimeout(() => - { - dcpExes.Clear(); - dcpExes.AddRange(kubernetesService.CreatedResources.OfType()); - return dcpExes.Count == 1; - }, TestConstants.DefaultOrchestratorTestTimeout); - Assert.True(haveExes, $"Expected one executable but instead got {dcpExes.Count}"); - - var exe = Assert.Single(dcpExes, e => e.AppModelResourceName == "proj"); + var exe = GetCreatedExecutableForResource(kubernetesService, "proj"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); // The launch config should have been applied in CreateExecutableAsync (not PrepareProjectExecutables) @@ -3681,16 +4195,7 @@ public async Task Project_NonProjectLaunchConfig_AnnotatorThrows_FallsBackToProc await appExecutor.RunApplicationAsync(); // Assert - List dcpExes = []; - var haveExes = RetryTillTrueOrTimeout(() => - { - dcpExes.Clear(); - dcpExes.AddRange(kubernetesService.CreatedResources.OfType()); - return dcpExes.Count == 1; - }, TestConstants.DefaultOrchestratorTestTimeout); - Assert.True(haveExes, $"Expected one executable but instead got {dcpExes.Count}"); - - var exe = Assert.Single(dcpExes, e => e.AppModelResourceName == "proj"); + var exe = GetCreatedExecutableForResource(kubernetesService, "proj"); // Should fall back to Process execution when the launch configuration producer throws Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); } @@ -3730,19 +4235,22 @@ public async Task Project_NonProjectLaunchConfig_UnsupportedByExtension_RunsInPr await appExecutor.RunApplicationAsync(); // Assert - List dcpExes = []; - var haveExes = RetryTillTrueOrTimeout(() => - { - dcpExes.Clear(); - dcpExes.AddRange(kubernetesService.CreatedResources.OfType()); - return dcpExes.Count == 1; - }, TestConstants.DefaultOrchestratorTestTimeout); - Assert.True(haveExes, $"Expected one executable but instead got {dcpExes.Count}"); - - var exe = Assert.Single(dcpExes, e => e.AppModelResourceName == "proj"); + var exe = GetCreatedExecutableForResource(kubernetesService, "proj"); Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); } + private static Executable GetCreatedExecutableForResource(TestKubernetesService kubernetesService, string appModelResourceName) + { + return Assert.Single(GetCreatedExecutablesForResource(kubernetesService, appModelResourceName)); + } + + private static List GetCreatedExecutablesForResource(TestKubernetesService kubernetesService, string appModelResourceName) + { + return [.. kubernetesService.CreatedResources + .OfType() + .Where(e => e.AppModelResourceName == appModelResourceName)]; + } + private static DcpExecutor CreateAppExecutor( DistributedApplicationModel distributedAppModel, IHostEnvironment? hostEnvironment = null, @@ -3750,7 +4258,8 @@ private static DcpExecutor CreateAppExecutor( IKubernetesService? kubernetesService = null, DcpOptions? dcpOptions = null, ResourceLoggerService? resourceLoggerService = null, - DcpExecutorEvents? events = null) + DcpExecutorEvents? events = null, + Hosting.Eventing.IDistributedApplicationEventing? distributedApplicationEventing = null) { if (configuration == null) { @@ -3781,7 +4290,15 @@ private static DcpExecutor CreateAppExecutor( }); var ks = kubernetesService ?? new TestKubernetesService(); var dcpEvts = events ?? new DcpExecutorEvents(); - var locations = new Locations(new FileSystemService(configuration)); + var fileSystemService = new FileSystemService(configuration); + var locations = new Locations(fileSystemService); + var aspireStoreDirectory = configuration[AspireStore.AspireStorePathKeyName]; + if (string.IsNullOrWhiteSpace(aspireStoreDirectory)) + { + aspireStoreDirectory = fileSystemService.TempDirectory.CreateTempSubdirectory("aspire-store").Path; + } + + var aspireStore = new AspireStore(Path.Join(aspireStoreDirectory, ".aspire"), fileSystemService); var hostEnv = hostEnvironment ?? new TestHostEnvironment(); var dcpDependencyCheckService = new TestDcpDependencyCheckService(); @@ -3794,6 +4311,7 @@ private static DcpExecutor CreateAppExecutor( new DistributedApplicationOptions(), executionContext, locations, + aspireStore, NullLogger.Instance, appResources); @@ -3815,7 +4333,7 @@ private static DcpExecutor CreateAppExecutor( distributedAppModel, ks, configuration, - new Hosting.Eventing.DistributedApplicationEventing(), + distributedApplicationEventing ?? new Hosting.Eventing.DistributedApplicationEventing(), Options.Create(dcpOptions), executionContext, resourceLoggerService, @@ -3884,6 +4402,27 @@ private static IEnumerable GetEnumerablePropertyValue(CustomResourceSnapsh return Assert.IsAssignableFrom>(property.Value); } + private static X509Certificate2 CreateTestCertificate() + { + using var rsa = RSA.Create(2048); + var request = new CertificateRequest( + new X500DistinguishedName("CN=test"), + rsa, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + var serialNumber = new byte[16]; + RandomNumberGenerator.Fill(serialNumber); + var generator = X509SignatureGenerator.CreateForRSA(rsa, RSASignaturePadding.Pkcs1); + + return request.Create( + request.SubjectName, + generator, + DateTimeOffset.Now, + DateTimeOffset.Now.AddYears(1), + serialNumber); + } + private sealed class TestExecutableResource(string directory) : ExecutableResource("TestExecutable", "test", directory); private sealed class TestOtherExecutableResource(string directory) : ExecutableResource("TestOtherExecutable", "test-other", directory); diff --git a/tests/Aspire.Hosting.Tests/Dcp/ResourceSnapshotBuilderTests.cs b/tests/Aspire.Hosting.Tests/Dcp/ResourceSnapshotBuilderTests.cs index 271f9f12ff2..c2ad5e9da6b 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/ResourceSnapshotBuilderTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/ResourceSnapshotBuilderTests.cs @@ -48,6 +48,66 @@ public void ExecutableSnapshotFallsBackToAnnotationValueWhenEffectiveArgMissing( Assert.Equal(["-port", DcpTemplateArgument], GetEnumerablePropertyValue(snapshot, KnownProperties.Resource.AppArgs).ToArray()); } + [Fact] + public void ExplicitStartExecutableSnapshotWithUnknownStateIsNotStarted() + { + var executable = Executable.Create("exe", "pwsh"); + executable.Spec.Start = false; + executable.Status = new ExecutableStatus + { + State = ExecutableState.Unknown + }; + + var snapshot = CreateSnapshotBuilder().ToSnapshot(executable, CreatePreviousSnapshot()); + + Assert.Equal(KnownResourceStates.NotStarted, snapshot.State?.Text); + } + + [Fact] + public void ExplicitStartExecutableStatusWithUnknownStateIsNotStarted() + { + var executable = Executable.Create("exe", "pwsh"); + executable.Spec.Start = false; + executable.Status = new ExecutableStatus + { + State = ExecutableState.Unknown + }; + + var status = DcpResourceWatcher.GetResourceStatus(executable); + + Assert.Equal(KnownResourceStates.NotStarted, status.State); + } + + [Fact] + public void ExplicitStartExecutableSnapshotWithEmptyStateIsNotStarted() + { + var executable = Executable.Create("exe", "pwsh"); + executable.Spec.Start = false; + executable.Status = new ExecutableStatus + { + State = "" + }; + + var snapshot = CreateSnapshotBuilder().ToSnapshot(executable, CreatePreviousSnapshot()); + + Assert.Equal(KnownResourceStates.NotStarted, snapshot.State?.Text); + } + + [Fact] + public void ExplicitStartExecutableStatusWithEmptyStateIsNotStarted() + { + var executable = Executable.Create("exe", "pwsh"); + executable.Spec.Start = false; + executable.Status = new ExecutableStatus + { + State = "" + }; + + var status = DcpResourceWatcher.GetResourceStatus(executable); + + Assert.Equal(KnownResourceStates.NotStarted, status.State); + } + private static Executable CreateExecutable(AppLaunchArgumentAnnotation[] launchArgumentAnnotations, IReadOnlyList effectiveArgs) { var executable = Executable.Create("exe", "pwsh"); diff --git a/tests/Aspire.Hosting.Tests/Dcp/TestKubernetesService.cs b/tests/Aspire.Hosting.Tests/Dcp/TestKubernetesService.cs index 949e127a3a6..a2f72f8b3f6 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/TestKubernetesService.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/TestKubernetesService.cs @@ -70,12 +70,19 @@ static T Clone(T r) // "Allocate" port for a service. if (res is Service svc) { - if (svc.Status is null) + // Container tunnel client services are proxyless, but unlike dynamic + // container endpoints they must be ready before the dependent container starts. + if (svc.Spec.AddressAllocationMode != AddressAllocationModes.Proxyless || + svc.Spec.Port is not null || + svc.Metadata.Annotations?.ContainsKey(CustomResource.ContainerTunnelInstanceName) is true) { - svc.Status = new ServiceStatus(); + if (svc.Status is null) + { + svc.Status = new ServiceStatus(); + } + svc.Status.EffectiveAddress = svc.Spec.Address ?? "localhost"; + svc.Status.EffectivePort = svc.Spec.Port ?? Interlocked.Increment(ref _nextPort); } - svc.Status.EffectiveAddress = svc.Spec.Address ?? "localhost"; - svc.Status.EffectivePort = svc.Spec.Port ?? Interlocked.Increment(ref _nextPort); } // Simulate proxy startup by marking it as running immediately. @@ -92,10 +99,15 @@ static T Clone(T r) lock (CreatedResources) { + var modifiedResources = AllocateProxylessContainerServicePorts(res); CreatedResources.Enqueue(res); foreach (var c in _watchChannels) { c.Writer.TryWrite((WatchEventType.Added, res)); + foreach (var modifiedResource in modifiedResources) + { + c.Writer.TryWrite((WatchEventType.Modified, modifiedResource)); + } } } @@ -113,6 +125,32 @@ public void PushResourceModified(CustomResource resource) } } + private List AllocateProxylessContainerServicePorts(CustomResource resource) + { + if (resource is not Container container || + container.TryGetAnnotationAsObjectList(CustomResource.ServiceProducerAnnotation, out var servicesProduced) is not true) + { + return []; + } + + var modifiedResources = new List(); + foreach (var serviceProduced in servicesProduced) + { + var service = CreatedResources.OfType().FirstOrDefault(s => s.Metadata.Name == serviceProduced.ServiceName); + if (service?.Spec.AddressAllocationMode != AddressAllocationModes.Proxyless || service.Spec.Port is not null || service.Status?.EffectivePort is not null) + { + continue; + } + + service.Status ??= new ServiceStatus(); + service.Status.EffectiveAddress = service.Spec.Address ?? "localhost"; + service.Status.EffectivePort = Interlocked.Increment(ref _nextPort); + modifiedResources.Add(service); + } + + return modifiedResources; + } + public async Task DeleteAsync(string name, string? namespaceParameter = null, CancellationToken cancellationToken = default) where T : CustomResource, IKubernetesStaticMetadata { try diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index 447477fa082..5f4971c2754 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -2,12 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIRECERTIFICATES001 +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; using System.Text.RegularExpressions; using System.Threading.Channels; +using Aspire.Dashboard.Model; using Aspire.Hosting.Diagnostics; using Aspire.TestUtilities; using Aspire.Hosting.Dcp; @@ -607,7 +609,7 @@ public async Task ExplicitStart_StartPersistentContainer() var containerBuilder = AddRedisContainer(testProgram.AppBuilder, notStartedResourceName) .WithContainerName(notStartedResourceName) - .WithLifetime(ContainerLifetime.Persistent) + .WithPersistentLifetime() .WithEndpoint(port: 6379, targetPort: 6379, name: "tcp", env: "REDIS_PORT") .WithExplicitStart(); @@ -730,9 +732,9 @@ public void TryAddWillNotAddTheSameLifecycleHook() } [Fact] - public async Task AllocatedPortsAssignedAfterHookRuns() + public async Task AfterEndpointsAllocatedLifecycleHookIsNotCalled() { - using var testProgram = CreateTestProgram("ports-assigned-after-hook-runs"); + using var testProgram = CreateTestProgram("after-endpoints-allocated-hook-not-called"); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); #pragma warning disable CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. testProgram.AppBuilder.Services.AddLifecycleHook(sp => new CheckAllocatedEndpointsLifecycleHook(tcs)); @@ -742,15 +744,7 @@ public async Task AllocatedPortsAssignedAfterHookRuns() await app.StartAsync().DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout); - var appModel = await tcs.Task.DefaultTimeout(TestConstants.DefaultOrchestratorTestTimeout); - - foreach (var item in appModel.Resources) - { - if (item is IResourceWithEndpoints resourceWithEndpoints) - { - Assert.True(resourceWithEndpoints.GetEndpoints().All(e => e.IsAllocated)); - } - } + Assert.False(tcs.Task.IsCompleted); } #pragma warning disable CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. @@ -1928,7 +1922,7 @@ public async Task PersistentNetworkCreatedIfPersistentContainers(bool createPers if (createPersistentContainer) { builder.AddContainer($"{testName}-persistent", RedisContainerImageTags.Image, RedisContainerImageTags.Tag) - .WithLifetime(ContainerLifetime.Persistent); + .WithPersistentLifetime(); } builder.AddContainer($"{testName}-nonpersistent", RedisContainerImageTags.Image, RedisContainerImageTags.Tag); @@ -1950,6 +1944,172 @@ public async Task PersistentNetworkCreatedIfPersistentContainers(bool createPers } } + [Fact] + [RequiresFeature(TestFeature.Docker)] + public async Task ParentProcessLifetimeScopesExecutableAndContainerToParentProcess() + { + const string testName = "parent-process-lifetime-scope"; + using var builder = TestDistributedApplicationBuilder.Create(_testOutputHelper); + using var parentProcess = Process.GetCurrentProcess(); + var parentProcessIdentity = DcpProcessMonitor.GetMonitorProcessIdentity(parentProcess); + + var container = AddRedisContainer(builder, $"{testName}-container") + .WithParentProcessLifetime(parentProcess.Id) + .WithExplicitStart(); + + var executable = builder.AddExecutable($"{testName}-executable", "dotnet", Environment.CurrentDirectory, "--info") + .WithParentProcessLifetime(parentProcess.Id) + .WithExplicitStart(); + + using var app = builder.Build(); + using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.DefaultOrchestratorTestLongTimeout); + var token = cts.Token; + + await app.StartAsync(token).DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout); + + await app.ResourceNotifications.WaitForResourceAsync(container.Resource.Name, KnownResourceStates.NotStarted, token) + .DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout); + await app.ResourceNotifications.WaitForResourceAsync(executable.Resource.Name, KnownResourceStates.NotStarted, token) + .DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout); + + var kubernetes = app.Services.GetRequiredService(); + var containers = await kubernetes.ListAsync(cancellationToken: token) + .DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout); + var dcpContainer = Assert.Single(containers, c => c.AppModelResourceName == container.Resource.Name); + Assert.True(dcpContainer.Spec.Persistent.GetValueOrDefault()); + Assert.Equal(parentProcessIdentity.ProcessId, dcpContainer.Spec.MonitorPid); + Assert.NotNull(dcpContainer.Spec.MonitorTimestamp); + Assert.InRange((dcpContainer.Spec.MonitorTimestamp.Value - parentProcessIdentity.Timestamp).Duration(), TimeSpan.Zero, TimeSpan.FromMilliseconds(1)); + Assert.False(dcpContainer.Spec.Start.GetValueOrDefault(true)); + + var executables = await kubernetes.ListAsync(cancellationToken: token) + .DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout); + var dcpExecutable = Assert.Single(executables, e => e.AppModelResourceName == executable.Resource.Name); + Assert.True(dcpExecutable.Spec.Persistent.GetValueOrDefault()); + Assert.Equal(parentProcessIdentity.ProcessId, dcpExecutable.Spec.MonitorPid); + Assert.NotNull(dcpExecutable.Spec.MonitorTimestamp); + Assert.InRange((dcpExecutable.Spec.MonitorTimestamp.Value - parentProcessIdentity.Timestamp).Duration(), TimeSpan.Zero, TimeSpan.FromMilliseconds(1)); + Assert.False(dcpExecutable.Spec.Start.GetValueOrDefault(true)); + Assert.Equal(ExecutionType.Process, dcpExecutable.Spec.ExecutionType); + } + + [Fact] + [RequiresFeature(TestFeature.Docker)] + public async Task ParentProcessLifetimeReusesResourcesAcrossAppRestartsAndStopsWhenParentExits() + { + const string testName = "parent-process-lifetime-reuse"; + var containerResourceName = $"{testName}-redis"; + var executableResourceName = $"{testName}-worker"; + + using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.ExtraLongTimeoutDuration); + var token = cts.Token; + + using var aspireStore = new TestTempDirectory(); + using var executableDirectory = new TestTempDirectory(); + var executableAppPath = DotnetFileAppProcess.WriteApp(executableDirectory, "worker.cs", """ + using System.Threading; + using System.Threading.Tasks; + + await Task.Delay(Timeout.InfiniteTimeSpan); + """); + + using var parentProcess = StartLongRunningProcess(); + try + { + var firstRun = await StartParentScopedResourcesAsync(parentProcess.Id, token); + await StopAndDisposeAppAsync(firstRun.App, token); + + var secondRun = await StartParentScopedResourcesAsync(parentProcess.Id, token); + try + { + Assert.Equal(firstRun.ContainerId, secondRun.ContainerId); + Assert.Equal(firstRun.ExecutablePid, secondRun.ExecutablePid); + + await KillProcessAsync(parentProcess, token); + + var rns = secondRun.App.Services.GetRequiredService(); + await Task.WhenAll( + rns.WaitForResourceAsync(containerResourceName, e => + { + var state = e.Snapshot.State?.Text; + // DCP stops parent-scoped containers and removes the DCP resource object, + // which the app observes as Unknown rather than an Exited status update. + // Tighten this to require Exited after DCP stops the container without + // removing the resource object. + return state == KnownResourceStates.Exited || state == ContainerState.Unknown; + }, token), + rns.WaitForResourceAsync(executableResourceName, e => + { + var state = e.Snapshot.State?.Text; + return state == KnownResourceStates.Finished || state == ExecutableState.Terminated; + }, token)) + .DefaultTimeout(TestConstants.ExtraLongTimeoutTimeSpan); + } + finally + { + await StopAndDisposeAppAsync(secondRun.App, token); + } + } + finally + { + await KillProcessAsync(parentProcess, token); + } + + async Task StartParentScopedResourcesAsync(int parentProcessId, CancellationToken cancellationToken) + { + var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(_testOutputHelper) + .WithTempAspireStore(aspireStore.Path) + .WithResourceCleanUp(false); + + AddRedisContainer(builder, containerResourceName) + .WithContainerName(containerResourceName) + .WithParentProcessLifetime(parentProcessId); + + builder.AddExecutable(executableResourceName, DotnetFileAppProcess.ExecutablePath, executableDirectory.Path, DotnetFileAppProcess.CreateArguments(executableAppPath)) + .WithParentProcessLifetime(parentProcessId); + + var app = builder.Build(); + try + { + await app.StartAsync(cancellationToken).DefaultTimeout(TestConstants.ExtraLongTimeoutTimeSpan); + + var rns = app.Services.GetRequiredService(); + var containerEvent = await rns.WaitForResourceAsync( + containerResourceName, + e => e.Snapshot.State?.Text == KnownResourceStates.Running && + GetResourcePropertyValue(e, KnownProperties.Container.Id) is string, + cancellationToken).DefaultTimeout(TestConstants.ExtraLongTimeoutTimeSpan); + var executableEvent = await rns.WaitForResourceAsync( + executableResourceName, + e => e.Snapshot.State?.Text == KnownResourceStates.Running && + GetResourcePropertyValue(e, KnownProperties.Executable.Pid) is int, + cancellationToken).DefaultTimeout(TestConstants.ExtraLongTimeoutTimeSpan); + + var containerId = Assert.IsType(GetResourcePropertyValue(containerEvent, KnownProperties.Container.Id)); + var executablePid = Assert.IsType(GetResourcePropertyValue(executableEvent, KnownProperties.Executable.Pid)); + + return new ParentScopedResourcesRun(app, containerId, executablePid); + } + catch + { + await app.DisposeAsync().AsTask().DefaultTimeout(TestConstants.ExtraLongTimeoutTimeSpan); + throw; + } + } + + static async Task StopAndDisposeAppAsync(DistributedApplication app, CancellationToken cancellationToken) + { + try + { + await app.StopAsync(cancellationToken).DefaultTimeout(TestConstants.ExtraLongTimeoutTimeSpan); + } + finally + { + await app.DisposeAsync().AsTask().DefaultTimeout(TestConstants.ExtraLongTimeoutTimeSpan); + } + } + } + [Fact] [RequiresFeature(TestFeature.Docker)] public async Task AfterResourcesCreatedLifecycleHookWorks() @@ -1974,6 +2134,7 @@ public async Task AfterResourcesCreatedLifecycleHookWorks() await app.StartAsync().DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout); await kubernetesLifecycle.HooksCompleted.DefaultTimeout(TestConstants.DefaultOrchestratorTestTimeout); + Assert.False(kubernetesLifecycle.AfterEndpointsAllocatedCalled); } [Fact] @@ -2103,31 +2264,59 @@ private static IResourceBuilder AddRedisContainer(IDistribute .WithImageRegistry(AspireTestContainerRegistry); } + private static object? GetResourcePropertyValue(ResourceEvent resourceEvent, string propertyName) + { + return resourceEvent.Snapshot.Properties.FirstOrDefault(p => p.Name == propertyName)?.Value; + } + + private static Process StartLongRunningProcess() + { + var startInfo = OperatingSystem.IsWindows() + ? new ProcessStartInfo("ping", "-t localhost") { CreateNoWindow = true } + : new ProcessStartInfo("tail", "-f /dev/null"); + + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + + var process = Process.Start(startInfo); + Assert.NotNull(process); + + return process; + } + + private static async Task KillProcessAsync(Process process, CancellationToken cancellationToken) + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + + await process.WaitForExitAsync(cancellationToken); + } + + private sealed record ParentScopedResourcesRun(DistributedApplication App, string ContainerId, int ExecutablePid); + #pragma warning disable CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. private sealed class KubernetesTestLifecycleHook : IDistributedApplicationLifecycleHook #pragma warning restore CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. { private readonly TaskCompletionSource _tcs = new(); - private readonly CountdownEvent _cevent = new(2); // AfterResourcesCreated and AfterEndpointsAllocated public IKubernetesService? KubernetesService { get; set; } + public bool AfterEndpointsAllocatedCalled { get; private set; } public Task HooksCompleted => _tcs.Task; - public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) + public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) { - if (_cevent.Signal()) - { - _tcs.TrySetResult(); - } + AfterEndpointsAllocatedCalled = true; + return Task.CompletedTask; } - public async Task AfterResourcesCreatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) + public Task AfterResourcesCreatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) { - if (_cevent.Signal()) - { - _tcs.TrySetResult(); - } + _tcs.TrySetResult(); + return Task.CompletedTask; } } diff --git a/tests/Aspire.Hosting.Tests/Eventing/DistributedApplicationBuilderEventingTests.cs b/tests/Aspire.Hosting.Tests/Eventing/DistributedApplicationBuilderEventingTests.cs index acbba2a8808..3aa55fd2d02 100644 --- a/tests/Aspire.Hosting.Tests/Eventing/DistributedApplicationBuilderEventingTests.cs +++ b/tests/Aspire.Hosting.Tests/Eventing/DistributedApplicationBuilderEventingTests.cs @@ -7,6 +7,8 @@ using Aspire.Hosting.Utils; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; namespace Aspire.Hosting.Tests.Eventing; @@ -259,11 +261,36 @@ public async Task LifeycleHookAnalogousEventsFire() await app.StartAsync(); var allFired = ManualResetEvent.WaitAll( - [beforeStartEventFired.WaitHandle, afterEndpointsAllocatedEventFired.WaitHandle, afterResourcesCreatedEventFired.WaitHandle], + [beforeStartEventFired.WaitHandle, afterResourcesCreatedEventFired.WaitHandle], TimeSpan.FromSeconds(10) ); Assert.True(allFired); + Assert.False(afterEndpointsAllocatedEventFired.IsSet); + await app.StopAsync(); + } + + [Fact] + public async Task ObsoleteAfterEndpointsAllocatedEventSubscriptionLogsWarning() + { + var testSink = new TestSink(); + + using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); + builder.Services.AddLogging(logging => logging.AddProvider(new TestLoggerProvider(testSink))); +#pragma warning disable CS0618 // Type or member is obsolete + builder.Eventing.Subscribe((e, ct) => Task.CompletedTask); +#pragma warning restore CS0618 // Type or member is obsolete + + using var app = builder.Build(); + await app.StartAsync(); + +#pragma warning disable CS0618 // Type or member is obsolete + Assert.Contains(testSink.Writes, w => + w.LogLevel == LogLevel.Warning && + w.Message?.Contains(nameof(AfterEndpointsAllocatedEvent), StringComparison.Ordinal) == true && + w.Message?.Contains(nameof(ResourceEndpointsAllocatedEvent), StringComparison.Ordinal) == true); +#pragma warning restore CS0618 // Type or member is obsolete + await app.StopAsync(); } diff --git a/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs b/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs index e1fdcf3ae82..ca7ff076eee 100644 --- a/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs +++ b/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREEXTENSION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. #pragma warning disable IDE0005 // Using directive is unnecessary. using Aspire.Hosting.Dcp.Model; @@ -72,6 +73,17 @@ public void WithWorkingDirectoryAllowsEmptyString() Assert.Equal(builder.AppHostDirectory, annotation.WorkingDirectory); } + [Fact] + public void WithPersistentLifetimeAddsPersistenceAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(); + var executable = builder.AddExecutable("myexe", "command", "workingdirectory") + .WithPersistentLifetime(); + + var annotation = executable.Resource.Annotations.OfType().Single(); + Assert.Equal(PersistenceMode.Persistent, annotation.Mode); + } + [Fact] public void WithDebugSupportAddsAnnotationInRunMode() { diff --git a/tests/Aspire.Hosting.Tests/PersistentContainerWarningTests.cs b/tests/Aspire.Hosting.Tests/PersistentContainerWarningTests.cs index b96873bcc8a..79befbc4c4b 100644 --- a/tests/Aspire.Hosting.Tests/PersistentContainerWarningTests.cs +++ b/tests/Aspire.Hosting.Tests/PersistentContainerWarningTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREUSERSECRETS001 +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.UserSecrets; @@ -26,7 +27,7 @@ public async Task PersistentContainerWithoutUserSecrets_LogsWarning() var resources = new ResourceCollection(); var container = new ContainerResource("my-container"); - container.Annotations.Add(new ContainerLifetimeAnnotation { Lifetime = ContainerLifetime.Persistent }); + container.Annotations.Add(new PersistenceAnnotation { Mode = PersistenceMode.Persistent }); resources.Add(container); var model = new DistributedApplicationModel(resources); @@ -50,7 +51,7 @@ public async Task PersistentContainerWithUserSecrets_DoesNotLogWarning() var resources = new ResourceCollection(); var container = new ContainerResource("my-container"); - container.Annotations.Add(new ContainerLifetimeAnnotation { Lifetime = ContainerLifetime.Persistent }); + container.Annotations.Add(new PersistenceAnnotation { Mode = PersistenceMode.Persistent }); resources.Add(container); var model = new DistributedApplicationModel(resources); diff --git a/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs b/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs new file mode 100644 index 00000000000..f83a5de5c79 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + +using Aspire.Hosting.Utils; + +namespace Aspire.Hosting.Tests; + +[Trait("Partition", "2")] +public class ProjectResourceBuilderExtensionTests +{ + [Fact] + public void WithPersistentLifetimeAddsPersistenceAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var project = builder.AddProject("project", options => options.ExcludeLaunchProfile = true) + .WithPersistentLifetime(); + + var annotation = project.Resource.Annotations.OfType().Single(); + Assert.Equal(PersistenceMode.Persistent, annotation.Mode); + } + + private sealed class TestProject : IProjectMetadata + { + public string ProjectPath => "test.csproj"; + } +} diff --git a/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs b/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs new file mode 100644 index 00000000000..ededfd678b9 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs @@ -0,0 +1,171 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + +using System.Diagnostics; +using Aspire.Hosting.Dcp; +using Aspire.Hosting.Utils; + +namespace Aspire.Hosting.Tests; + +[Trait("Partition", "2")] +public class ResourceBuilderLifetimeTests +{ + [Fact] + public void WithPersistentLifetimeAddsPersistenceAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var container = builder.AddContainer("container", "image") + .WithPersistentLifetime(); + + var annotation = container.Resource.Annotations.OfType().Single(); + Assert.Equal(PersistenceMode.Persistent, annotation.Mode); + } + + [Fact] + public void WithPersistentLifetimeRejectsUnsupportedResourceTypes() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var parameter = builder.AddParameter("parameter"); + + void ConfigureLifetime() => parameter.WithPersistentLifetime(); + + var exception = Assert.Throws((Action)ConfigureLifetime); + Assert.Contains("does not support lifetime configuration", exception.Message); + } + + [Fact] + public void WithPersistentLifetimeReplacesPersistenceAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var container = builder.AddContainer("container", "image") + .WithParentProcessLifetime(Environment.ProcessId) + .WithPersistentLifetime(); + + var annotation = container.Resource.Annotations.OfType().Single(); + Assert.Equal(PersistenceMode.Persistent, annotation.Mode); + Assert.Null(annotation.ParentProcessId); + Assert.Null(annotation.ParentProcessTimestamp); + } + + [Fact] + public void WithParentProcessLifetimeReplacesExistingPersistenceAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var container = builder.AddContainer("container", "image") + .WithPersistentLifetime(); + + container.WithParentProcessLifetime(Environment.ProcessId); + + var annotation = Assert.Single(container.Resource.Annotations.OfType()); + Assert.Equal(PersistenceMode.ParentProcess, annotation.Mode); + Assert.Equal(Environment.ProcessId, annotation.ParentProcessId); + Assert.NotNull(annotation.ParentProcessTimestamp); + } + + [Fact] + public void WithLifetimeOfMatchesSourceResourceLifetime() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var source = builder.AddContainer("source", "image") + .WithPersistentLifetime(); + var container = builder.AddContainer("container", "image") + .WithSessionLifetime() + .WithLifetimeOf(source); + + Assert.Equal(Lifetime.Persistent, container.Resource.GetLifetimeType()); + var annotation = Assert.Single(container.Resource.Annotations.OfType()); + Assert.Equal(PersistenceMode.Resource, annotation.Mode); + Assert.Same(source.Resource, annotation.SourceResource); + + source.WithSessionLifetime(); + + Assert.Equal(Lifetime.Session, container.Resource.GetLifetimeType()); + } + + [Fact] + public void WithLifetimeOfMatchesSourceParentProcessLifetime() + { + using var builder = TestDistributedApplicationBuilder.Create(); + using var parentProcess = Process.GetCurrentProcess(); + var parentProcessIdentity = DcpProcessMonitor.GetMonitorProcessIdentity(parentProcess); + + var source = builder.AddContainer("source", "image") + .WithParentProcessLifetime(parentProcess.Id); + var container = builder.AddContainer("container", "image") + .WithLifetimeOf(source); + + Assert.True(container.Resource.TryGetParentProcessLifetime(out var parentProcessId, out var parentProcessTimestamp)); + Assert.Equal(parentProcessIdentity.ProcessId, parentProcessId); + Assert.Equal(parentProcessIdentity.Timestamp, parentProcessTimestamp); + + source.WithSessionLifetime(); + + Assert.False(container.Resource.TryGetParentProcessLifetime(out _, out _)); + } + + [Fact] + public void ExplicitLifetimeOverridesWithLifetimeOf() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var source = builder.AddContainer("source", "image") + .WithSessionLifetime(); + var container = builder.AddContainer("container", "image") + .WithLifetimeOf(source) + .WithPersistentLifetime(); + + source.WithSessionLifetime(); + + Assert.Equal(Lifetime.Persistent, container.Resource.GetLifetimeType()); + } + + [Fact] + public void WithLifetimeOfRejectsUnsupportedResourceTypes() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var parameter = builder.AddParameter("parameter"); + var container = builder.AddContainer("container", "image"); + + void ConfigureLifetime() => parameter.WithLifetimeOf(container); + + var exception = Assert.Throws((Action)ConfigureLifetime); + Assert.Contains("does not support lifetime configuration", exception.Message); + } + + [Fact] + public void WithLifetimeOfDetectsCircularReferences() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var containerA = builder.AddContainer("container-a", "image"); + var containerB = builder.AddContainer("container-b", "image") + .WithLifetimeOf(containerA); + containerA.WithLifetimeOf(containerB); + + var exception = Assert.Throws(() => containerA.Resource.GetLifetimeType()); + Assert.Contains("circular lifetime reference", exception.Message); + } + + [Fact] + public void WithSessionLifetimeReplacesPersistenceAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var container = builder.AddContainer("container", "image") + .WithParentProcessLifetime(Environment.ProcessId) + .WithSessionLifetime(); + + var annotation = container.Resource.Annotations.OfType().Single(); + Assert.Equal(PersistenceMode.Session, annotation.Mode); + Assert.Null(annotation.ParentProcessId); + Assert.Null(annotation.ParentProcessTimestamp); + } +} diff --git a/tests/Aspire.Hosting.Tests/WithEndpointTests.cs b/tests/Aspire.Hosting.Tests/WithEndpointTests.cs index 055e4676a7a..11852cbe782 100644 --- a/tests/Aspire.Hosting.Tests/WithEndpointTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEndpointTests.cs @@ -1,11 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net.Sockets; +using System.Reflection; using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; -using System.Net.Sockets; namespace Aspire.Hosting.Tests; @@ -15,6 +16,147 @@ public class WithEndpointTests // copied from /src/Shared/StringComparers.cs to avoid ambiguous reference since StringComparers exists internally in multiple Hosting assemblies. private static StringComparison EndpointAnnotationName => StringComparison.OrdinalIgnoreCase; + [Fact] + public void EndpointIsProxiedBinaryCompatibilityOverloadsExist() + { + Assert.NotNull(GetPublicStaticMethod( + typeof(ResourceBuilderExtensions), + nameof(ResourceBuilderExtensions.WithEndpoint), + typeof(IResourceBuilder<>), + typeof(int?), + typeof(int?), + typeof(string), + typeof(string), + typeof(string), + typeof(bool), + typeof(bool?), + typeof(ProtocolType?))); + + Assert.NotNull(GetPublicStaticMethod( + typeof(ResourceBuilderExtensions), + nameof(ResourceBuilderExtensions.WithEndpoint), + typeof(IResourceBuilder<>), + typeof(int?), + typeof(int?), + typeof(string), + typeof(string), + typeof(string), + typeof(bool), + typeof(bool?))); + + Assert.NotNull(GetPublicStaticMethod( + typeof(ResourceBuilderExtensions), + nameof(ResourceBuilderExtensions.WithHttpEndpoint), + typeof(IResourceBuilder<>), + typeof(int?), + typeof(int?), + typeof(string), + typeof(string), + typeof(bool))); + + Assert.NotNull(GetPublicStaticMethod( + typeof(ResourceBuilderExtensions), + nameof(ResourceBuilderExtensions.WithHttpsEndpoint), + typeof(IResourceBuilder<>), + typeof(int?), + typeof(int?), + typeof(string), + typeof(string), + typeof(bool))); + + var withEndpointProxySupport = GetPublicStaticMethod( + typeof(ContainerResourceBuilderExtensions), + nameof(ContainerResourceBuilderExtensions.WithEndpointProxySupport), + typeof(IResourceBuilder<>), + typeof(bool)); + + Assert.NotNull(withEndpointProxySupport); + var constraint = Assert.Single(withEndpointProxySupport.GetGenericArguments()[0].GetGenericParameterConstraints()); + Assert.Equal(typeof(ContainerResource), constraint); + + Assert.NotNull(GetPublicConstructor( + typeof(EndpointAnnotation), + typeof(ProtocolType), + typeof(string), + typeof(string), + typeof(string), + typeof(int?), + typeof(int?), + typeof(bool?), + typeof(bool))); + + Assert.NotNull(GetPublicConstructor( + typeof(EndpointAnnotation), + typeof(ProtocolType), + typeof(NetworkIdentifier), + typeof(string), + typeof(string), + typeof(string), + typeof(int?), + typeof(int?), + typeof(bool?), + typeof(bool))); + + var isProxiedProperty = typeof(EndpointAnnotation).GetProperty(nameof(EndpointAnnotation.IsProxied), BindingFlags.Public | BindingFlags.Instance); + Assert.NotNull(isProxiedProperty); + Assert.Equal(typeof(bool), isProxiedProperty.PropertyType); + } + + [Fact] + public void EndpointAnnotationConstructorOmissionLeavesProxySettingUnset() + { + var endpoint = new EndpointAnnotation(ProtocolType.Tcp); + + Assert.True(endpoint.IsProxied); + Assert.Null(endpoint.IsExplicitlyProxied); + } + + [Fact] + public void EndpointAnnotationBoolCompatibilityConstructorPreservesExplicitProxySetting() + { + var constructor = GetPublicConstructor( + typeof(EndpointAnnotation), + typeof(ProtocolType), + typeof(string), + typeof(string), + typeof(string), + typeof(int?), + typeof(int?), + typeof(bool?), + typeof(bool)); + + var endpoint = Assert.IsType(constructor!.Invoke([ProtocolType.Tcp, "http", null, "http", null, null, null, true])); + + Assert.True(endpoint.IsProxied); + Assert.True(endpoint.IsExplicitlyProxied); + } + + [Fact] + public void WithHttpEndpointOmittedIsProxiedLeavesProxySettingUnset() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var container = builder.AddContainer("app", "image") + .WithHttpEndpoint(name: "http"); + + var endpoint = Assert.Single(container.Resource.Annotations.OfType()); + Assert.True(endpoint.IsProxied); + Assert.Null(endpoint.IsExplicitlyProxied); + } + + [Fact] + public void WithHttpEndpointBoolCompatibilityOverloadPreservesExplicitProxySetting() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var container = builder.AddContainer("app", "image"); + ResourceBuilderExtensions.WithHttpEndpoint(container, port: null, targetPort: null, name: "http", env: null, isProxied: true); + + var endpoint = Assert.Single(container.Resource.Annotations.OfType()); + Assert.True(endpoint.IsProxied); + Assert.True(endpoint.IsExplicitlyProxied); + } + [Fact] public void WithEndpointInvokesCallback() { @@ -32,6 +174,28 @@ public void WithEndpointInvokesCallback() Assert.Equal(2000, endpoint.Port); } + private static MethodInfo? GetPublicStaticMethod(Type declaringType, string methodName, params Type[] parameterTypes) + { + return declaringType.GetMethods(BindingFlags.Public | BindingFlags.Static) + .SingleOrDefault(method => + method.Name == methodName && + method.GetParameters().Select(parameter => NormalizeGenericParameterType(parameter.ParameterType)).SequenceEqual(parameterTypes)); + } + + private static ConstructorInfo? GetPublicConstructor(Type declaringType, params Type[] parameterTypes) + { + return declaringType.GetConstructors(BindingFlags.Public | BindingFlags.Instance) + .SingleOrDefault(constructor => + constructor.GetParameters().Select(parameter => parameter.ParameterType).SequenceEqual(parameterTypes)); + } + + private static Type NormalizeGenericParameterType(Type parameterType) + { + return parameterType.IsGenericType && parameterType.GetGenericTypeDefinition() == typeof(IResourceBuilder<>) + ? typeof(IResourceBuilder<>) + : parameterType; + } + [Fact] public void WithEndpointMakesTargetPortEqualToPortIfProxyless() { @@ -881,12 +1045,10 @@ public void WithEndpointUpdateDoesNotChangeScheme() } [Fact] - public void WithEndpointUpdateDoesNotChangeIsProxiedBackToTrue() + public void WithEndpointUpdateCanSetIsProxiedToTrue() { var builder = DistributedApplication.CreateBuilder(); - // isProxied defaults to true in the method signature, so passing true - // on update can't be distinguished from the default — it's a no-op. builder.AddContainer("mycontainer", "myimage") .WithHttpEndpoint(port: 8080, isProxied: false) .WithHttpEndpoint(port: 9090, isProxied: true); @@ -896,7 +1058,7 @@ public void WithEndpointUpdateDoesNotChangeIsProxiedBackToTrue() var resource = Assert.Single(builder.Resources.OfType()); var endpoint = Assert.Single(resource.Annotations.OfType(), e => e.Name == "http"); Assert.Equal(9090, endpoint.Port); - Assert.False(endpoint.IsProxied); + Assert.True(endpoint.IsProxied); } [Fact] diff --git a/tests/Aspire.Hosting.Tests/WithUrlsTests.cs b/tests/Aspire.Hosting.Tests/WithUrlsTests.cs index cf0affb3bc8..686a3ac06df 100644 --- a/tests/Aspire.Hosting.Tests/WithUrlsTests.cs +++ b/tests/Aspire.Hosting.Tests/WithUrlsTests.cs @@ -473,7 +473,7 @@ static string FormatUrls(IEnumerable urls) => var watchTask = Task.Run(async () => { - await foreach (var notification in rns.WatchAsync().DefaultTimeout()) + await foreach (var notification in rns.WatchAsync().DefaultTimeout(TestConstants.LongTimeoutDuration)) { if (notification.Resource == servicea.Resource && notification.Snapshot.Urls.Length > 0) { @@ -589,7 +589,7 @@ static string FormatUrls(IEnumerable urls) => var watchTask = Task.Run(async () => { - await foreach (var notification in rns.WatchAsync().DefaultTimeout()) + await foreach (var notification in rns.WatchAsync().DefaultTimeout(TestConstants.LongTimeoutDuration)) { if (notification.Resource == custom.Resource && notification.Snapshot.Urls.Length > 0) { diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/Go/apphost.go index f619af99784..9ae0b14a099 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/Go/apphost.go @@ -40,7 +40,7 @@ func main() { } pgContainer.RunAsContainer(&aspire.RunAsContainerOptions{ ConfigureContainer: func(container aspire.PostgresServerResource) { - container.WithLifetime(aspire.ContainerLifetimePersistent) + container.WithPersistentLifetime() }, }) if pgContainer.Err() != nil { diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/Java/AppHost.java index ab3c5358286..578af44dba5 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/Java/AppHost.java @@ -13,7 +13,7 @@ void main() throws Exception { var pgContainer = builder.addAzurePostgresFlexibleServer("pg-container"); pgContainer.runAsContainer((container) -> { // Exercise PostgresServerResource builder methods within the callback - container.withLifetime(ContainerLifetime.PERSISTENT); + container.withPersistentLifetime(); }); // 5) addDatabase on container-mode server var dbContainer = pgContainer.addDatabase("containerdb"); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/TypeScript/apphost.ts b/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/TypeScript/apphost.ts index 127c865566b..9f3667ac408 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/TypeScript/apphost.ts +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/TypeScript/apphost.ts @@ -1,4 +1,4 @@ -import { createBuilder, ContainerLifetime } from './.modules/aspire.js'; +import { createBuilder } from './.modules/aspire.js'; const builder = await createBuilder(); @@ -17,7 +17,7 @@ const pgContainer = await builder.addAzurePostgresFlexibleServer("pg-container") await pgContainer.runAsContainer({ configureContainer: async (container) => { // Exercise PostgresServerResource builder methods within the callback - await container.withLifetime(ContainerLifetime.Persistent); + await container.withPersistentLifetime(); }, }); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/Go/apphost.go index bc8bf5318ce..9efff9e9aae 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/Go/apphost.go @@ -47,7 +47,7 @@ func main() { builder.AddMilvus("milvus-cfg").WithConfigurationFile("./milvus.yaml") milvusChained := builder.AddMilvus("milvus-chained") - milvusChained.WithLifetime(aspire.ContainerLifetimePersistent) + milvusChained.WithPersistentLifetime() milvusChained.WithDataVolume(&aspire.WithDataVolumeOptions{Name: aspire.StringPtr("milvus-chained-data")}) milvusChained.WithAttu() diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/Java/AppHost.java index ddc8bf2596f..bb79d67dcc6 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/Java/AppHost.java @@ -39,7 +39,7 @@ void main() throws Exception { .withConfigurationFile("./milvus.yaml"); // ── 14. Fluent chaining: multiple With* methods ──────────────────────────── var milvusChained = builder.addMilvus("milvus-chained"); - milvusChained.withLifetime(ContainerLifetime.PERSISTENT); + milvusChained.withPersistentLifetime(); milvusChained.withDataVolume(new WithDataVolumeOptions().name("milvus-chained-data")); milvusChained.withAttu(); // ── 15. withReference: use Milvus database from a container resource ─────── diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/TypeScript/apphost.ts b/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/TypeScript/apphost.ts index a0b4880fe21..cebc4531b1b 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/TypeScript/apphost.ts +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/TypeScript/apphost.ts @@ -1,7 +1,7 @@ // Aspire TypeScript AppHost — Milvus integration validation // Exercises every exported member of Aspire.Hosting.Milvus -import { createBuilder, ContainerLifetime } from './.modules/aspire.js'; +import { createBuilder } from './.modules/aspire.js'; const builder = await createBuilder(); @@ -55,7 +55,7 @@ await builder.addMilvus("milvus-cfg") // ── 14. Fluent chaining: multiple With* methods ──────────────────────────── await builder.addMilvus("milvus-chained") - .withLifetime(ContainerLifetime.Persistent) + .withPersistentLifetime() .withDataVolume({ name: "milvus-chained-data" }) .withAttu(); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/Go/apphost.go index cfd8d6c6afa..c0b0ef9df21 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/Go/apphost.go @@ -40,7 +40,7 @@ func main() { builder.AddMongoDB("mongo-custom-pass", &aspire.AddMongoDBOptions{Password: &customPassword}) mongoChained := builder.AddMongoDB("mongo-chained") - mongoChained.WithLifetime(aspire.ContainerLifetimePersistent) + mongoChained.WithPersistentLifetime() mongoChained.WithDataVolume(&aspire.WithDataVolumeOptions{Name: aspire.StringPtr("mongo-chained-data")}) mongoChained.AddDatabase("app-db") diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/Java/AppHost.java index a23f0ac5539..26a9b08222f 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/Java/AppHost.java @@ -29,7 +29,7 @@ void main() throws Exception { builder.addMongoDB("mongo-custom-pass", new AddMongoDBOptions().password(customPassword)); // Test 9: Chained configuration - multiple With* methods var mongoChained = builder.addMongoDB("mongo-chained"); - mongoChained.withLifetime(ContainerLifetime.PERSISTENT); + mongoChained.withPersistentLifetime(); mongoChained.withDataVolume(new WithDataVolumeOptions().name("mongo-chained-data")); // Test 10: Add multiple databases to same server mongoChained.addDatabase("app-db"); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/TypeScript/apphost.ts b/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/TypeScript/apphost.ts index 6d9ad0ec02a..cd4da41014f 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/TypeScript/apphost.ts +++ b/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/TypeScript/apphost.ts @@ -1,7 +1,7 @@ // Aspire TypeScript AppHost // For more information, see: https://aspire.dev -import { createBuilder, ContainerLifetime } from './.modules/aspire.js'; +import { createBuilder } from './.modules/aspire.js'; const builder = await createBuilder(); @@ -40,7 +40,7 @@ await builder.addMongoDB("mongo-custom-pass", { password: customPassword }); // Test 9: Chained configuration - multiple With* methods const mongoChained = await builder.addMongoDB("mongo-chained") - .withLifetime(ContainerLifetime.Persistent) + .withPersistentLifetime() .withDataVolume({ name: "mongo-chained-data" }); // Test 10: Add multiple databases to same server @@ -57,4 +57,4 @@ const _userName = await mongo.userNameReference(); // Build and run the app const _cstr = await mongo.connectionStringExpression(); const _databases = mongo.databases; -await builder.build().run(); \ No newline at end of file +await builder.build().run(); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Nats/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting.Nats/Go/apphost.go index c9242b7af5e..8a0d815c82f 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Nats/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Nats/Go/apphost.go @@ -25,7 +25,7 @@ func main() { Name: aspire.StringPtr("nats-data"), IsReadOnly: aspire.BoolPtr(false), }) - nats2.WithLifetime(aspire.ContainerLifetimePersistent) + nats2.WithPersistentLifetime() if err = nats2.Err(); err != nil { log.Fatalf(aspire.FormatError(err)) } diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Nats/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting.Nats/Java/AppHost.java index 446ff018365..f7286037d66 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Nats/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Nats/Java/AppHost.java @@ -14,7 +14,7 @@ void main() throws Exception { var nats2 = builder.addNats("messaging2", new AddNatsOptions().port(4223.0)) .withJetStream() .withDataVolume(new WithDataVolumeOptions().name("nats-data").isReadOnly(false)) - .withLifetime(ContainerLifetime.PERSISTENT); + .withPersistentLifetime(); // withDataBindMount - bind mount a host directory var nats3 = builder.addNats("messaging3"); nats3.withDataBindMount("/tmp/nats-data"); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Nats/TypeScript/apphost.ts b/tests/PolyglotAppHosts/Aspire.Hosting.Nats/TypeScript/apphost.ts index aa220a1856c..965d5286151 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Nats/TypeScript/apphost.ts +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Nats/TypeScript/apphost.ts @@ -1,7 +1,7 @@ // Aspire TypeScript AppHost — NATS integration validation // Exercises all [AspireExport] methods for Aspire.Hosting.Nats -import { createBuilder, ContainerLifetime } from './.modules/aspire.js'; +import { createBuilder } from './.modules/aspire.js'; const builder = await createBuilder(); @@ -18,7 +18,7 @@ await nats.withDataVolume(); const nats2 = await builder.addNats("messaging2", { port: 4223 }) .withJetStream() .withDataVolume({ name: "nats-data", isReadOnly: false }) - .withLifetime(ContainerLifetime.Persistent); + .withPersistentLifetime(); // withDataBindMount — bind mount a host directory const nats3 = await builder.addNats("messaging3"); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/Go/apphost.go index e77ebdcef51..7cfdd623207 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/Go/apphost.go @@ -54,7 +54,7 @@ func main() { oracle.WithReference(otherOracle) oracle3 := builder.AddOracle("oracledb3") - oracle3.WithLifetime(aspire.ContainerLifetimePersistent) + oracle3.WithPersistentLifetime() oracle3.WithDataVolume(&aspire.WithDataVolumeOptions{Name: aspire.StringPtr("oracle3-data")}) oracle3.AddDatabase("chaineddb") if err = oracle3.Err(); err != nil { diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/Java/AppHost.java index 4bc9727c505..d7e067afc2b 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/Java/AppHost.java @@ -33,7 +33,7 @@ void main() throws Exception { oracle.withReference(otherOracle, new WithReferenceOptions()); // ---- Fluent chaining: multiple methods chained ---- var oracle3 = builder.addOracle("oracledb3"); - oracle3.withLifetime(ContainerLifetime.PERSISTENT); + oracle3.withPersistentLifetime(); oracle3.withDataVolume("oracle3-data"); oracle3.addDatabase("chaineddb"); // ---- Property access on OracleDatabaseServerResource ---- diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/TypeScript/apphost.ts b/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/TypeScript/apphost.ts index 72bbcec782f..e59d9a79297 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/TypeScript/apphost.ts +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/TypeScript/apphost.ts @@ -1,7 +1,7 @@ // Aspire TypeScript AppHost - Oracle Integration Validation // Validates all [AspireExport] methods for Aspire.Hosting.Oracle -import { createBuilder, ContainerLifetime } from './.modules/aspire.js'; +import { createBuilder } from './.modules/aspire.js'; const builder = await createBuilder(); @@ -46,7 +46,7 @@ await oracle.withReference(otherOracle); // ---- Fluent chaining: multiple methods chained ---- const oracle3 = await builder.addOracle("oracledb3") - .withLifetime(ContainerLifetime.Persistent) + .withPersistentLifetime() .withDataVolume({ name: "oracle3-data" }); await oracle3.addDatabase("chaineddb"); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/Go/apphost.go index e878a87404e..ce839881957 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/Go/apphost.go @@ -20,7 +20,7 @@ func main() { } rabbitmq2 := builder.AddRabbitMQ("messaging2") - rabbitmq2.WithLifetime(aspire.ContainerLifetimePersistent) + rabbitmq2.WithPersistentLifetime() rabbitmq2.WithDataVolume() rabbitmq2.WithManagementPlugin(&aspire.WithManagementPluginOptions{Port: aspire.Float64Ptr(15673)}) if err = rabbitmq2.Err(); err != nil { diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/Java/AppHost.java index 703bd992de5..9b44c72f885 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/Java/AppHost.java @@ -6,7 +6,7 @@ void main() throws Exception { rabbitmq.withDataVolume(); rabbitmq.withManagementPlugin(); var rabbitmq2 = builder.addRabbitMQ("messaging2"); - rabbitmq2.withLifetime(ContainerLifetime.PERSISTENT); + rabbitmq2.withPersistentLifetime(); rabbitmq2.withDataVolume(); rabbitmq2.withManagementPlugin(15673.0); // ---- Property access on RabbitMQServerResource ---- diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/TypeScript/apphost.ts b/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/TypeScript/apphost.ts index abf93afcf6d..edce80695dc 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/TypeScript/apphost.ts +++ b/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/TypeScript/apphost.ts @@ -1,4 +1,4 @@ -import { createBuilder, ContainerLifetime } from './.modules/aspire.js'; +import { createBuilder } from './.modules/aspire.js'; const builder = await createBuilder(); @@ -8,7 +8,7 @@ await rabbitmq.withManagementPlugin(); const rabbitmq2 = await builder .addRabbitMQ("messaging2") - .withLifetime(ContainerLifetime.Persistent) + .withPersistentLifetime() .withDataVolume() .withManagementPlugin({ port: 15673 }); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/Go/apphost.go index 31aa32841e2..8d06e4b6f63 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/Go/apphost.go @@ -27,7 +27,7 @@ func main() { builder.AddSqlServer("sql-custom-pass", &aspire.AddSqlServerOptions{Password: &customPassword}) sqlChained := builder.AddSqlServer("sql-chained") - sqlChained.WithLifetime(aspire.ContainerLifetimePersistent) + sqlChained.WithPersistentLifetime() sqlChained.WithDataVolume(&aspire.WithDataVolumeOptions{Name: aspire.StringPtr("sql-chained-data")}) sqlChained.WithHostPort(12433) diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/Java/AppHost.java index 13241434091..c44e77df036 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/Java/AppHost.java @@ -17,7 +17,7 @@ void main() throws Exception { builder.addSqlServer("sql-custom-pass", new AddSqlServerOptions().password(customPassword)); // Test 6: Chained configuration - multiple With* methods var sqlChained = builder.addSqlServer("sql-chained"); - sqlChained.withLifetime(ContainerLifetime.PERSISTENT); + sqlChained.withPersistentLifetime(); sqlChained.withDataVolume(new WithDataVolumeOptions().name("sql-chained-data")); sqlChained.withHostPort(12433.0); // Test 7: Add multiple databases to same server diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/TypeScript/apphost.ts b/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/TypeScript/apphost.ts index a931df9c4b0..1ceacc413e2 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/TypeScript/apphost.ts +++ b/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/TypeScript/apphost.ts @@ -1,4 +1,4 @@ -import { createBuilder, ContainerLifetime } from './.modules/aspire.js'; +import { createBuilder } from './.modules/aspire.js'; const builder = await createBuilder(); @@ -22,7 +22,7 @@ await builder.addSqlServer("sql-custom-pass", { password: customPassword }); // Test 6: Chained configuration - multiple With* methods const sqlChained = await builder.addSqlServer("sql-chained") - .withLifetime(ContainerLifetime.Persistent) + .withPersistentLifetime() .withDataVolume({ name: "sql-chained-data" }) .withHostPort({ port: 12433 });