Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public async Task GetOrCreateResourceAsync(AzureBicepResource resource, Provisio
if (BicepUtilities.GetExistingResourceGroup(resource) is { } existingResourceGroup)
{
var existingResourceGroupName = existingResourceGroup is ParameterResource parameterResource
? parameterResource.Value
? (await parameterResource.GetValueAsync(cancellationToken).ConfigureAwait(false))!
: (string)existingResourceGroup;
var response = await context.Subscription.GetResourceGroups().GetAsync(existingResourceGroupName, cancellationToken).ConfigureAwait(false);
resourceGroup = response.Value;
Expand Down
16 changes: 8 additions & 8 deletions src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,14 @@ public static IResourceBuilder<T> WithPhpMyAdmin<T>(this IResourceBuilder<T> bui
.WithHttpEndpoint(targetPort: 80, name: "http")
.ExcludeFromManifest();

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

if (!mySqlInstances.Any())
{
// No-op if there are no MySql resources present.
return Task.CompletedTask;
return;
}

if (mySqlInstances.Count() == 1)
Expand All @@ -225,12 +225,12 @@ public static IResourceBuilder<T> WithPhpMyAdmin<T>(this IResourceBuilder<T> bui
// 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);
context.EnvironmentVariables.Add("PMA_PASSWORD", singleInstance.PasswordParameter);
});
}
else
{
var tempConfigFile = WritePhpMyAdminConfiguration(mySqlInstances);
var tempConfigFile = await WritePhpMyAdminConfiguration(mySqlInstances, ct).ConfigureAwait(false);

try
{
Expand Down Expand Up @@ -258,8 +258,6 @@ public static IResourceBuilder<T> WithPhpMyAdmin<T>(this IResourceBuilder<T> bui
}
}
}

return Task.CompletedTask;
});

configureContainer?.Invoke(phpMyAdminContainerBuilder);
Expand Down Expand Up @@ -346,7 +344,7 @@ public static IResourceBuilder<MySqlServerResource> WithInitFiles(this IResource
return builder.WithContainerFiles(initPath, importFullPath);
}

private static string WritePhpMyAdminConfiguration(IEnumerable<MySqlServerResource> mySqlInstances)
private static async Task<string> WritePhpMyAdminConfiguration(IEnumerable<MySqlServerResource> mySqlInstances, CancellationToken cancellationToken)
{
// This temporary file is not used by the container, it will be copied and then deleted
var filePath = Path.GetTempFileName();
Expand All @@ -360,14 +358,16 @@ private static string WritePhpMyAdminConfiguration(IEnumerable<MySqlServerResour
foreach (var mySqlInstance in mySqlInstances)
{
var endpoint = mySqlInstance.PrimaryEndpoint;
var pwd = await mySqlInstance.PasswordParameter.GetValueAsync(cancellationToken).ConfigureAwait(false);

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]['password'] = '{pwd}';");
writer.WriteLine($"$cfg['Servers'][$i]['AllowNoPassword'] = true;");
writer.WriteLine();
}
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting.Nats/NatsBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public static IResourceBuilder<NatsServerResource> AddNats(this IDistributedAppl
AuthOpts = new()
{
Username = await nats.UserNameReference.GetValueAsync(ct).ConfigureAwait(false),
Password = nats.PasswordParameter!.Value,
Password = await nats.PasswordParameter!.GetValueAsync(ct).ConfigureAwait(false),
}
};

Expand Down
36 changes: 22 additions & 14 deletions src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,18 +203,18 @@ public static IResourceBuilder<T> WithPgAdmin<T>(this IResourceBuilder<T> builde

pgAdminContainerBuilder.WithContainerFiles(
destinationPath: "/pgadmin4",
callback: (context, _) =>
callback: async (context, cancellationToken) =>
{
var appModel = context.ServiceProvider.GetRequiredService<DistributedApplicationModel>();
var postgresInstances = builder.ApplicationBuilder.Resources.OfType<PostgresServerResource>();

return Task.FromResult<IEnumerable<ContainerFileSystemItem>>([
return [
new ContainerFile
{
Name = "servers.json",
Contents = WritePgAdminServerJson(postgresInstances),
Contents = await WritePgAdminServerJson(postgresInstances, cancellationToken).ConfigureAwait(false),
},
]);
];
});

configureContainer?.Invoke(pgAdminContainerBuilder);
Expand Down Expand Up @@ -313,25 +313,25 @@ public static IResourceBuilder<PostgresServerResource> WithPgWeb(this IResourceB

pgwebContainerBuilder.WithContainerFiles(
destinationPath: "/",
callback: (context, _) =>
callback: async (context, ct) =>
{
var appModel = context.ServiceProvider.GetRequiredService<DistributedApplicationModel>();
var postgresInstances = builder.ApplicationBuilder.Resources.OfType<PostgresDatabaseResource>();

// Add the bookmarks to the pgweb container
return Task.FromResult<IEnumerable<ContainerFileSystemItem>>([
return [
new ContainerDirectory
{
Name = ".pgweb",
Entries = [
new ContainerDirectory
{
Name = "bookmarks",
Entries = WritePgWebBookmarks(postgresInstances),
Entries = await WritePgWebBookmarks(postgresInstances, ct).ConfigureAwait(false)
},
],
},
]);
];
});

return builder;
Expand Down Expand Up @@ -489,21 +489,25 @@ public static IResourceBuilder<PostgresServerResource> WithHostPort(this IResour
});
}

private static IEnumerable<ContainerFileSystemItem> WritePgWebBookmarks(IEnumerable<PostgresDatabaseResource> postgresInstances)
private static async Task<IEnumerable<ContainerFileSystemItem>> WritePgWebBookmarks(IEnumerable<PostgresDatabaseResource> postgresInstances, CancellationToken cancellationToken)
{
var bookmarkFiles = new List<ContainerFileSystemItem>();

foreach (var postgresDatabase in postgresInstances)
{
var user = postgresDatabase.Parent.UserNameParameter?.Value ?? "postgres";
var user = postgresDatabase.Parent.UserNameParameter is null
? "postgres"
: await postgresDatabase.Parent.UserNameParameter.GetValueAsync(cancellationToken).ConfigureAwait(false);

var password = await postgresDatabase.Parent.PasswordParameter.GetValueAsync(cancellationToken).ConfigureAwait(false) ?? "password";

// 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
var fileContent = $"""
host = "{postgresDatabase.Parent.Name}"
port = {postgresDatabase.Parent.PrimaryEndpoint.TargetPort}
user = "{user}"
password = "{postgresDatabase.Parent.PasswordParameter.Value}"
password = "{password}"
database = "{postgresDatabase.DatabaseName}"
sslmode = "disable"
""";
Expand All @@ -518,7 +522,7 @@ private static IEnumerable<ContainerFileSystemItem> WritePgWebBookmarks(IEnumera
return bookmarkFiles;
}

private static string WritePgAdminServerJson(IEnumerable<PostgresServerResource> postgresInstances)
private static async Task<string> WritePgAdminServerJson(IEnumerable<PostgresServerResource> postgresInstances, CancellationToken cancellationToken)
{
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true });
Expand All @@ -531,6 +535,10 @@ private static string WritePgAdminServerJson(IEnumerable<PostgresServerResource>
foreach (var postgresInstance in postgresInstances)
{
var endpoint = postgresInstance.PrimaryEndpoint;
var userName = postgresInstance.UserNameParameter is null
? "postgres"
: await postgresInstance.UserNameParameter.GetValueAsync(cancellationToken).ConfigureAwait(false);
var password = await postgresInstance.PasswordParameter.GetValueAsync(cancellationToken).ConfigureAwait(false);

writer.WriteStartObject($"{serverIndex}");
writer.WriteString("Name", postgresInstance.Name);
Expand All @@ -539,10 +547,10 @@ private static string WritePgAdminServerJson(IEnumerable<PostgresServerResource>
// 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("Username", userName);
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.WriteString("PasswordExecCommand", $"echo '{password}'"); // HACK: Generating a pass file and playing around with chmod is too painful.
writer.WriteEndObject();

serverIndex++;
Expand Down
11 changes: 5 additions & 6 deletions src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,14 @@ public static IResourceBuilder<RedisResource> WithRedisCommander(this IResourceB
.WithHttpEndpoint(targetPort: 8081, name: "http")
.ExcludeFromManifest();

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

if (!redisInstances.Any())
{
// No-op if there are no Redis resources present.
return Task.CompletedTask;
return;
}

var hostsVariableBuilder = new StringBuilder();
Expand All @@ -177,14 +177,13 @@ public static IResourceBuilder<RedisResource> WithRedisCommander(this IResourceB
var hostString = $"{(hostsVariableBuilder.Length > 0 ? "," : string.Empty)}{redisInstance.Name}:{redisInstance.Name}:{redisInstance.PrimaryEndpoint.TargetPort}:0";
if (redisInstance.PasswordParameter is not null)
{
hostString += $":{redisInstance.PasswordParameter.Value}";
var password = await redisInstance.PasswordParameter.GetValueAsync(ct).ConfigureAwait(false);
hostString += $":{password}";
}
hostsVariableBuilder.Append(hostString);
}

resourceBuilder.WithEnvironment("REDIS_HOSTS", hostsVariableBuilder.ToString());

return Task.CompletedTask;
});

configureContainer?.Invoke(resourceBuilder);
Expand Down Expand Up @@ -244,7 +243,7 @@ public static IResourceBuilder<RedisResource> WithRedisInsight(this IResourceBui
context.EnvironmentVariables.Add($"RI_REDIS_ALIAS{counter}", redisInstance.Name);
if (redisInstance.PasswordParameter is not null)
{
context.EnvironmentVariables.Add($"RI_REDIS_PASSWORD{counter}", redisInstance.PasswordParameter.Value);
context.EnvironmentVariables.Add($"RI_REDIS_PASSWORD{counter}", redisInstance.PasswordParameter);
}

counter++;
Expand Down
14 changes: 11 additions & 3 deletions src/Aspire.Hosting/ApplicationModel/ParameterResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ public ParameterResource(string name, Func<ParameterDefault?, string> callback,
/// <summary>
/// Gets the value of the parameter.
/// </summary>
public string Value
public string Value => GetValueAsync(default).AsTask().GetAwaiter().GetResult()!;

internal string ValueInternal
{
get
{
Expand Down Expand Up @@ -79,14 +81,20 @@ internal string ConfigurationKey
/// </summary>
internal TaskCompletionSource<string>? WaitForValueTcs { get; set; }

async ValueTask<string?> IValueProvider.GetValueAsync(CancellationToken cancellationToken)
/// <summary>
/// Gets the value of the parameter asynchronously, waiting if necessary for the value to be set.
/// </summary>
/// <param name="cancellationToken">The cancellation token to observe while waiting for the value.</param>
/// <returns>A task that represents the asynchronous operation, containing the value of the parameter.</returns>
public async ValueTask<string?> GetValueAsync(CancellationToken cancellationToken)
{
if (WaitForValueTcs is not null)
{
// Wait for the value to be set if the task completion source is available.
return await WaitForValueTcs.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
}

return Value;
// In publish mode, there's no WaitForValueTcs set.
return ValueInternal;
}
}
7 changes: 6 additions & 1 deletion src/Aspire.Hosting/ExternalServiceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ private static IResourceBuilder<ExternalServiceResource> AddExternalServiceImpl(
.WithInitialState(new CustomResourceSnapshot
{
ResourceType = "ExternalService",
State = KnownResourceStates.Waiting,
Properties = []
})
.ExcludeFromManifest();
Expand All @@ -104,7 +105,9 @@ private static IResourceBuilder<ExternalServiceResource> AddExternalServiceImpl(
if (uri is null)
{
// If the URI is not set, it means we are using a parameterized URL
var url = resource.UrlParameter?.Value;
var url = resource.UrlParameter is null
? null
: await resource.UrlParameter.GetValueAsync(ct).ConfigureAwait(false);

if (!ExternalServiceResource.UrlIsValidForExternalService(url, out uri, out var message))
{
Expand Down Expand Up @@ -182,6 +185,8 @@ public static IResourceBuilder<ExternalServiceResource> WithHttpHealthCheck(this
{
var uri = builder.Resource.Uri;

// OK accessing the parameter here synchronously as this should only activate once the resource is running

if (uri is null && !Uri.TryCreate(builder.Resource.UrlParameter?.Value, UriKind.Absolute, out uri)
|| (uri?.Scheme != "http" && uri?.Scheme != "https"))
{
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ private async Task ProcessParameterAsync(ParameterResource parameterResource)
{
try
{
var value = parameterResource.Value ?? "";
var value = parameterResource.ValueInternal ?? "";

await notificationService.PublishUpdateAsync(parameterResource, s =>
{
Expand Down
11 changes: 7 additions & 4 deletions src/Aspire.Hosting/ResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,16 @@ public static IResourceBuilder<T> WithEnvironment<T>(this IResourceBuilder<T> bu
}
else if (externalService.Resource.UrlParameter is not null)
{
builder.WithEnvironment(context =>
builder.WithEnvironment(async context =>
{
var url = await externalService.Resource.UrlParameter.GetValueAsync(context.CancellationToken).ConfigureAwait(false);

// In publish mode we can't validate the parameter value so we'll just use it without validating.
if (!context.ExecutionContext.IsPublishMode && !ExternalServiceResource.UrlIsValidForExternalService(externalService.Resource.UrlParameter.Value, out var _, out var message))
if (!context.ExecutionContext.IsPublishMode && !ExternalServiceResource.UrlIsValidForExternalService(url, out var _, out var message))
{
throw new DistributedApplicationException($"The URL parameter '{externalService.Resource.UrlParameter.Name}' for the external service '{externalService.Resource.Name}' is invalid: {message}");
}

context.EnvironmentVariables[name] = externalService.Resource.UrlParameter;
});
}
Expand Down Expand Up @@ -528,15 +531,15 @@ public static IResourceBuilder<TDestination> WithReference<TDestination>(this IR
}
else if (externalService.Resource.UrlParameter is not null)
{
builder.WithEnvironment(context =>
builder.WithEnvironment(async context =>
{
string envVarName;
if (context.ExecutionContext.IsPublishMode)
{
// In publish mode we can't read the parameter value to get the scheme so use 'default'
envVarName = $"services__{externalService.Resource.Name}__default__0";
}
else if (ExternalServiceResource.UrlIsValidForExternalService(externalService.Resource.UrlParameter.Value, out var uri, out var message))
else if (ExternalServiceResource.UrlIsValidForExternalService(await externalService.Resource.UrlParameter.GetValueAsync(context.CancellationToken).ConfigureAwait(false), out var uri, out var message))
{
envVarName = $"services__{externalService.Resource.Name}__{uri.Scheme}__0";
}
Expand Down
Loading