Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow disabling proxy for persistent lifetime container endpoints #7232

Merged
merged 11 commits into from
Jan 28, 2025
Merged
11 changes: 4 additions & 7 deletions src/Aspire.Hosting.Kafka/KafkaBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,19 +110,16 @@ public static IResourceBuilder<KafkaServerResource> WithKafkaUI(this IResourceBu
.WithHttpEndpoint(targetPort: KafkaUIPort)
.ExcludeFromManifest();

builder.ApplicationBuilder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>((e, ct) =>
builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(kafkaUi, (e, ct) =>
danegsta marked this conversation as resolved.
Show resolved Hide resolved
{
var kafkaResources = builder.ApplicationBuilder.Resources.OfType<KafkaServerResource>();

int i = 0;
foreach (var kafkaResource in kafkaResources)
{
if (kafkaResource.InternalEndpoint.IsAllocated)
danegsta marked this conversation as resolved.
Show resolved Hide resolved
{
var endpoint = kafkaResource.InternalEndpoint;
int index = i;
kafkaUiBuilder.WithEnvironment(context => ConfigureKafkaUIContainer(context, endpoint, index));
}
var endpoint = kafkaResource.InternalEndpoint;
int index = i;
kafkaUiBuilder.WithEnvironment(context => ConfigureKafkaUIContainer(context, endpoint, index));

i++;
}
Expand Down
46 changes: 20 additions & 26 deletions src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public static IResourceBuilder<T> WithPhpMyAdmin<T>(this IResourceBuilder<T> bui
.WithBindMount(configurationTempFileName, "/etc/phpmyadmin/config.user.inc.php")
.ExcludeFromManifest();

builder.ApplicationBuilder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>((e, ct) =>
builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(phpMyAdminContainer, (e, ct) =>
{
var mySqlInstances = builder.ApplicationBuilder.Resources.OfType<MySqlServerResource>();

Expand All @@ -125,18 +125,15 @@ public static IResourceBuilder<T> WithPhpMyAdmin<T>(this IResourceBuilder<T> bui
if (mySqlInstances.Count() == 1)
{
var singleInstance = mySqlInstances.Single();
if (singleInstance.PrimaryEndpoint.IsAllocated)
var endpoint = singleInstance.PrimaryEndpoint;
phpMyAdminContainerBuilder.WithEnvironment(context =>
{
var endpoint = singleInstance.PrimaryEndpoint;
phpMyAdminContainerBuilder.WithEnvironment(context =>
{
// PhpMyAdmin assumes MySql is being accessed over a default Aspire container network and hardcodes the resource address
// This will need to be refactored once updated service discovery APIs are available
context.EnvironmentVariables.Add("PMA_HOST", $"{endpoint.Resource.Name}:{endpoint.TargetPort}");
context.EnvironmentVariables.Add("PMA_USER", "root");
context.EnvironmentVariables.Add("PMA_PASSWORD", singleInstance.PasswordParameter.Value);
});
}
// PhpMyAdmin assumes MySql is being accessed over a default Aspire container network and hardcodes the resource address
// This will need to be refactored once updated service discovery APIs are available
context.EnvironmentVariables.Add("PMA_HOST", $"{endpoint.Resource.Name}:{endpoint.TargetPort}");
context.EnvironmentVariables.Add("PMA_USER", "root");
context.EnvironmentVariables.Add("PMA_PASSWORD", singleInstance.PasswordParameter.Value);
});
}
else
{
Expand All @@ -149,20 +146,17 @@ public static IResourceBuilder<T> WithPhpMyAdmin<T>(this IResourceBuilder<T> bui
writer.WriteLine();
foreach (var mySqlInstance in mySqlInstances)
{
if (mySqlInstance.PrimaryEndpoint.IsAllocated)
{
var endpoint = mySqlInstance.PrimaryEndpoint;
writer.WriteLine("$i++;");
// PhpMyAdmin assumes MySql is being accessed over a default Aspire container network and hardcodes the resource address
// This will need to be refactored once updated service discovery APIs are available
writer.WriteLine($"$cfg['Servers'][$i]['host'] = '{endpoint.Resource.Name}:{endpoint.TargetPort}';");
writer.WriteLine($"$cfg['Servers'][$i]['verbose'] = '{mySqlInstance.Name}';");
writer.WriteLine($"$cfg['Servers'][$i]['auth_type'] = 'cookie';");
writer.WriteLine($"$cfg['Servers'][$i]['user'] = 'root';");
writer.WriteLine($"$cfg['Servers'][$i]['password'] = '{mySqlInstance.PasswordParameter.Value}';");
writer.WriteLine($"$cfg['Servers'][$i]['AllowNoPassword'] = true;");
writer.WriteLine();
}
var endpoint = mySqlInstance.PrimaryEndpoint;
writer.WriteLine("$i++;");
// PhpMyAdmin assumes MySql is being accessed over a default Aspire container network and hardcodes the resource address
// This will need to be refactored once updated service discovery APIs are available
writer.WriteLine($"$cfg['Servers'][$i]['host'] = '{endpoint.Resource.Name}:{endpoint.TargetPort}';");
writer.WriteLine($"$cfg['Servers'][$i]['verbose'] = '{mySqlInstance.Name}';");
writer.WriteLine($"$cfg['Servers'][$i]['auth_type'] = 'cookie';");
writer.WriteLine($"$cfg['Servers'][$i]['user'] = 'root';");
writer.WriteLine($"$cfg['Servers'][$i]['password'] = '{mySqlInstance.PasswordParameter.Value}';");
writer.WriteLine($"$cfg['Servers'][$i]['AllowNoPassword'] = true;");
writer.WriteLine();
}
writer.WriteLine("$cfg['DefaultServer'] = 1;");
writer.WriteLine("?>");
Expand Down
35 changes: 16 additions & 19 deletions src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public static IResourceBuilder<T> WithPgAdmin<T>(this IResourceBuilder<T> builde
.WithHttpHealthCheck("/browser")
.ExcludeFromManifest();

builder.ApplicationBuilder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>((e, ct) =>
builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(pgAdminContainer, (e, ct) =>
{
var serverFileMount = pgAdminContainer.Annotations.OfType<ContainerMountAnnotation>().Single(v => v.Target == "/pgadmin4/servers.json");
var postgresInstances = builder.ApplicationBuilder.Resources.OfType<PostgresServerResource>();
Expand All @@ -177,23 +177,20 @@ public static IResourceBuilder<T> WithPgAdmin<T>(this IResourceBuilder<T> builde

foreach (var postgresInstance in postgresInstances)
{
if (postgresInstance.PrimaryEndpoint.IsAllocated)
{
var endpoint = postgresInstance.PrimaryEndpoint;

writer.WriteStartObject($"{serverIndex}");
writer.WriteString("Name", postgresInstance.Name);
writer.WriteString("Group", "Servers");
// PgAdmin assumes Postgres is being accessed over a default Aspire container network and hardcodes the resource address
// This will need to be refactored once updated service discovery APIs are available
writer.WriteString("Host", endpoint.Resource.Name);
writer.WriteNumber("Port", (int)endpoint.TargetPort!);
writer.WriteString("Username", postgresInstance.UserNameParameter?.Value ?? "postgres");
writer.WriteString("SSLMode", "prefer");
writer.WriteString("MaintenanceDB", "postgres");
writer.WriteString("PasswordExecCommand", $"echo '{postgresInstance.PasswordParameter.Value}'"); // HACK: Generating a pass file and playing around with chmod is too painful.
writer.WriteEndObject();
}
var endpoint = postgresInstance.PrimaryEndpoint;

writer.WriteStartObject($"{serverIndex}");
writer.WriteString("Name", postgresInstance.Name);
writer.WriteString("Group", "Servers");
// PgAdmin assumes Postgres is being accessed over a default Aspire container network and hardcodes the resource address
// This will need to be refactored once updated service discovery APIs are available
writer.WriteString("Host", endpoint.Resource.Name);
writer.WriteNumber("Port", (int)endpoint.TargetPort!);
writer.WriteString("Username", postgresInstance.UserNameParameter?.Value ?? "postgres");
writer.WriteString("SSLMode", "prefer");
writer.WriteString("MaintenanceDB", "postgres");
writer.WriteString("PasswordExecCommand", $"echo '{postgresInstance.PasswordParameter.Value}'"); // HACK: Generating a pass file and playing around with chmod is too painful.
writer.WriteEndObject();

serverIndex++;
}
Expand Down Expand Up @@ -293,7 +290,7 @@ public static IResourceBuilder<PostgresServerResource> WithPgWeb(this IResourceB

pgwebContainerBuilder.WithRelationship(builder.Resource, "PgWeb");

builder.ApplicationBuilder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>(async (e, ct) =>
builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(pgwebContainer, async (e, ct) =>
{
var adminResource = builder.ApplicationBuilder.Resources.OfType<PgWebContainerResource>().Single();
var serverFileMount = adminResource.Annotations.OfType<ContainerMountAnnotation>().Single(v => v.Target == "/.pgweb/bookmarks");
Expand Down
13 changes: 5 additions & 8 deletions src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public static IResourceBuilder<RedisResource> WithRedisCommander(this IResourceB
.WithHttpEndpoint(targetPort: 8081, name: "http")
.ExcludeFromManifest();

builder.ApplicationBuilder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>((e, ct) =>
builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(resource, (e, ct) =>
{
var redisInstances = builder.ApplicationBuilder.Resources.OfType<RedisResource>();

Expand All @@ -109,13 +109,10 @@ public static IResourceBuilder<RedisResource> WithRedisCommander(this IResourceB

foreach (var redisInstance in redisInstances)
{
if (redisInstance.PrimaryEndpoint.IsAllocated)
{
// Redis Commander assumes Redis is being accessed over a default Aspire container network and hardcodes the resource address
// This will need to be refactored once updated service discovery APIs are available
var hostString = $"{(hostsVariableBuilder.Length > 0 ? "," : string.Empty)}{redisInstance.Name}:{redisInstance.Name}:{redisInstance.PrimaryEndpoint.TargetPort}:0";
hostsVariableBuilder.Append(hostString);
}
// Redis Commander assumes Redis is being accessed over a default Aspire container network and hardcodes the resource address
// This will need to be refactored once updated service discovery APIs are available
var hostString = $"{(hostsVariableBuilder.Length > 0 ? "," : string.Empty)}{redisInstance.Name}:{redisInstance.Name}:{redisInstance.PrimaryEndpoint.TargetPort}:0";
hostsVariableBuilder.Append(hostString);
}

resourceBuilder.WithEnvironment("REDIS_HOSTS", hostsVariableBuilder.ToString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ namespace Aspire.Hosting.ApplicationModel;
/// method. This event provides access to the <see cref="IServiceProvider"/> interface to resolve dependencies including
/// <see cref="DistributedApplicationModel"/> service which is passed in as an argument
/// in <see cref="Aspire.Hosting.Lifecycle.IDistributedApplicationLifecycleHook.AfterEndpointsAllocatedAsync(Aspire.Hosting.ApplicationModel.DistributedApplicationModel, CancellationToken)"/>.
/// Endpoint allocation can be asynchronous and this event is published after all endpoints have been allocated, which may happen after resources have been created.
/// </remarks>
/// <example>
/// Subscribe to the <see cref="AfterEndpointsAllocatedEvent"/> event and resolve the distributed application model.
Expand Down
47 changes: 10 additions & 37 deletions src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ namespace Aspire.Hosting.ApplicationModel;
public sealed class EndpointAnnotation : IResourceAnnotation
{
private string? _transport;
private int? _port;
private bool _portSetToNull;
private int? _targetPort;
private bool _targetPortSetToNull;

/// <summary>
/// Initializes a new instance of <see cref="EndpointAnnotation"/>.
/// </summary>
Expand All @@ -30,8 +27,8 @@ public sealed class EndpointAnnotation : IResourceAnnotation
/// <param name="port">Desired port for the service.</param>
/// <param name="targetPort">This is the port the resource is listening on. If the endpoint is used for the container, it is the container port.</param>
/// <param name="isExternal">Indicates that this endpoint should be exposed externally at publish time.</param>
/// <param name="isProxied">Specifies if the endpoint will be proxied by DCP. Defaults to true.</param>
public EndpointAnnotation(ProtocolType protocol, string? uriScheme = null, string? transport = null, [EndpointName] string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool isProxied = true)
/// <param name="isProxied">Specifies if the endpoint will be proxied by DCP. Defaults to null.</param>
public EndpointAnnotation(ProtocolType protocol, string? uriScheme = null, string? transport = null, [EndpointName] string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool? isProxied = null)
danegsta marked this conversation as resolved.
Show resolved Hide resolved
{
// If the URI scheme is null, we'll adopt either udp:// or tcp:// based on the
// protocol. If the name is null, we'll use the URI scheme as the default. This
Expand All @@ -47,8 +44,8 @@ public EndpointAnnotation(ProtocolType protocol, string? uriScheme = null, strin
UriScheme = uriScheme;
_transport = transport;
Name = name;
_port = port;
_targetPort = targetPort;
Port = port;
TargetPort = targetPort;
IsExternal = isExternal ?? false;
IsProxied = isProxied;
}
Expand All @@ -66,39 +63,15 @@ public EndpointAnnotation(ProtocolType protocol, string? uriScheme = null, strin
/// <summary>
/// Desired port for the service
/// </summary>
public int? Port
{
// For proxy-less Endpoints the client port and target port should be the same.
// Note that this is just a "sensible default"--the consumer of the EndpointAnnotation is free
// to change Port and TargetPort after the annotation is created, but if the final values are inconsistent,
// the associated resource may fail to run.
// It also depends on what the EndpointAnnotation is applied to.
// In the Container case the TargetPort is the port that the process listens on inside the container,
// and the Port is the host interface port, so it is fine for them to be different.
get => _port ?? (IsProxied || _portSetToNull ? null : _targetPort);
set
{
_port = value;
_portSetToNull = value == null;
}
}
public int? Port { get; set; }

/// <summary>
/// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port.
/// </summary>
/// <remarks>
/// Defaults to <see cref="Port"/>.
/// </remarks>
public int? TargetPort
{
// See comment on the Port setter, as this is the reciprocal logic
get => _targetPort ?? (IsProxied || _targetPortSetToNull ? null : _port);
set
{
_targetPort = value;
_targetPortSetToNull = value == null;
}
}
public int? TargetPort { get; set; }

/// <summary>
/// If a service is URI-addressable, this property will contain the URI scheme to use for constructing service URI.
Expand Down Expand Up @@ -126,10 +99,10 @@ public string Transport

/// <summary>
/// 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 true means the endpoint will be proxied by DCP. False means the endpoint will be handled and exposed by the resource. Null will apply per-resource defaults.
/// </summary>
/// <remarks>Defaults to <c>true</c>.</remarks>
public bool IsProxied { get; set; } = true;
/// <remarks>Defaults to <c>null</c>.</remarks>
public bool? IsProxied { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the endpoint is from a launch profile.
Expand Down
12 changes: 11 additions & 1 deletion src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ public static int GetReplicaCount(this IResource resource)
/// Gets the lifetime type of the container for the specified resource.
/// Defaults to <see cref="ContainerLifetime.Session"/> if no <see cref="ContainerLifetimeAnnotation"/> is found.
/// </summary>
/// <param name="resource">The resource to the get the ContainerLifetimeType for.</param>
/// <param name="resource">The resource to get the ContainerLifetimeType for.</param>
/// <returns>
/// The <see cref="ContainerLifetime"/> from the <see cref="ContainerLifetimeAnnotation"/> for the resource (if the annotation exists).
/// Defaults to <see cref="ContainerLifetime.Session"/> if the annotation is not set.
Expand All @@ -335,6 +335,16 @@ internal static ContainerLifetime GetContainerLifetimeType(this IResource resour
return ContainerLifetime.Session;
}

/// <summary>
/// Determines whether endpoints for a service should be proxied by default. Returns true for non-container resources container resources with a lifetime other than <see cref="ContainerLifetime.Persistent"/>.
/// </summary>
/// <param name="resource">The resource to determine default endpoint proxy behavior for.</param>
/// <returns>True if resource endpoints should be proxied by default, false otherwise.</returns>
internal static bool ShouldProxyEndpointsByDefault(this IResource resource)
{
return !resource.IsContainer() || resource.GetContainerLifetimeType() != ContainerLifetime.Persistent;
}

/// <summary>
/// Get the top resource in the resource hierarchy.
/// e.g. for a AzureBlobStorageResource, the top resource is the AzureStorageResource.
Expand Down
Loading
Loading