|
// 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.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
#pragma warning disable ASPIRECOMPUTE001
#pragma warning disable ASPIRECOMPUTE002
#pragma warning disable ASPIRECOMPUTE003
namespace Aspire.Hosting.ApplicationModel;
/// <summary>
/// Provides extension methods for the <see cref="IResource"/> interface.
/// </summary>
public static class ResourceExtensions
{
/// <summary>
/// Attempts to get the last annotation of the specified type from the resource.
/// </summary>
/// <typeparam name="T">The type of the annotation to get.</typeparam>
/// <param name="resource">The resource to get the annotation from.</param>
/// <param name="annotation">When this method returns, contains the last annotation of the specified type from the resource, if found; otherwise, the default value for <typeparamref name="T"/>.</param>
/// <returns><see langword="true"/> if the last annotation of the specified type was found in the resource; otherwise, <see langword="false"/>.</returns>
public static bool TryGetLastAnnotation<T>(this IResource resource, [NotNullWhen(true)] out T? annotation) where T : IResourceAnnotation
{
if (resource.Annotations.OfType<T>().LastOrDefault() is { } lastAnnotation)
{
annotation = lastAnnotation;
return true;
}
else
{
annotation = default;
return false;
}
}
/// <summary>
/// Attempts to retrieve all annotations of the specified type from the given resource.
/// </summary>
/// <typeparam name="T">The type of annotation to retrieve.</typeparam>
/// <param name="resource">The resource to retrieve annotations from.</param>
/// <param name="result">When this method returns, contains the annotations of the specified type, if found; otherwise, <see langword="null"/>.</param>
/// <returns><see langword="true"/> if annotations of the specified type were found; otherwise, <see langword="false"/>.</returns>
public static bool TryGetAnnotationsOfType<T>(this IResource resource, [NotNullWhen(true)] out IEnumerable<T>? result) where T : IResourceAnnotation
{
var matchingTypeAnnotations = resource.Annotations.OfType<T>();
if (matchingTypeAnnotations.Any())
{
result = matchingTypeAnnotations.ToArray();
return true;
}
else
{
result = null;
return false;
}
}
/// <summary>
/// Gets whether <paramref name="resource"/> has an annotation of type <typeparamref name="T"/>
/// </summary>
/// <typeparam name="T">The type of annotation to retrieve.</typeparam>
/// <param name="resource">The resource to retrieve annotations from.</param>
/// <returns><see langword="true"/> if an annotation of the specified type was found; otherwise, <see langword="false"/>.</returns>
public static bool HasAnnotationOfType<T>(this IResource resource) where T : IResourceAnnotation
{
return resource.Annotations.Any(a => a is T);
}
/// <summary>
/// Attempts to retrieve all annotations of the specified type from the given resource including from parents.
/// </summary>
/// <typeparam name="T">The type of annotation to retrieve.</typeparam>
/// <param name="resource">The resource to retrieve annotations from.</param>
/// <param name="result">When this method returns, contains the annotations of the specified type, if found; otherwise, <see langword="null"/>.</param>
/// <returns><see langword="true"/> if annotations of the specified type were found; otherwise, <see langword="false"/>.</returns>
public static bool TryGetAnnotationsIncludingAncestorsOfType<T>(this IResource resource, [NotNullWhen(true)] out IEnumerable<T>? result) where T : IResourceAnnotation
{
if (resource is IResourceWithParent)
{
List<T>? annotations = null;
while (true)
{
foreach (var annotation in resource.Annotations.OfType<T>())
{
annotations ??= [];
annotations.Add(annotation);
}
if (resource is IResourceWithParent child)
{
resource = child.Parent;
}
else
{
break;
}
}
result = annotations;
return annotations is not null;
}
return TryGetAnnotationsOfType(resource, out result);
}
/// <summary>
/// Gets whether <paramref name="resource"/> or its ancestors have an annotation of type <typeparamref name="T"/>
/// </summary>
/// <typeparam name="T">The type of annotation to retrieve.</typeparam>
/// <param name="resource">The resource to retrieve annotations from.</param>
/// <returns><see langword="true"/> if an annotation of the specified type was found; otherwise, <see langword="false"/>.</returns>
public static bool HasAnnotationIncludingAncestorsOfType<T>(this IResource resource) where T : IResourceAnnotation
{
if (resource is IResourceWithParent)
{
while (true)
{
if (HasAnnotationOfType<T>(resource))
{
return true;
}
if (resource is IResourceWithParent child)
{
resource = child.Parent;
}
else
{
break;
}
}
return false;
}
return HasAnnotationOfType<T>(resource);
}
/// <summary>
/// Attempts to get the environment variables from the given resource.
/// </summary>
/// <param name="resource">The resource to get the environment variables from.</param>
/// <param name="environmentVariables">The environment variables retrieved from the resource, if any.</param>
/// <returns>True if the environment variables were successfully retrieved, false otherwise.</returns>
public static bool TryGetEnvironmentVariables(this IResource resource, [NotNullWhen(true)] out IEnumerable<EnvironmentCallbackAnnotation>? environmentVariables)
{
return TryGetAnnotationsOfType(resource, out environmentVariables);
}
/// <summary>
/// Gets a <see cref="IResourceExecutionConfigurationBuilder"/> for the given resource.
/// </summary>
/// <param name="resource">The resource to generate configuration for</param>
/// <returns>A <see cref="IResourceExecutionConfigurationBuilder"/> instance for the given resource.</returns>
/// <remarks>
/// <para>
/// This method is useful for building resource execution configurations (command line arguments and environment variables)
/// in a fluent manner. Individual configuration sources can be added to the builder before finalizing the configuration to
/// allow only supported configuration sources to be applied in a given execution context (run vs. publish, etc).
/// </para>
/// <para>
/// In particular, this is used to allow certificate-related features to contribute to the final config, but only in execution
/// contexts where they're supported.
/// </para>
/// <example>
/// <code>
/// var resolvedConfiguration = await myResource.ExecutionConfigurationBuilder()
/// .WithArguments()
/// .WithEnvironmentVariables()
/// .BuildAsync(executionContext, resourceLogger: null, cancellationToken: cancellationToken)
/// .ConfigureAwait(false);
///
/// foreach (var argument in resolveConfiguration.Arguments)
/// {
/// Console.WriteLine($"Argument: {argument.Value}");
/// }
/// </code>
/// </example>
/// </remarks>
public static IResourceExecutionConfigurationBuilder ExecutionConfigurationBuilder(this IResource resource)
{
return ResourceExecutionConfigurationBuilder.Create(resource);
}
/// <summary>
/// Get the environment variables from the given resource.
/// </summary>
/// <param name="resource">The resource to get the environment variables from.</param>
/// <param name="applicationOperation">The context in which the AppHost is being executed.</param>
/// <returns>The environment variables retrieved from the resource.</returns>
/// <remarks>
/// This method is useful when you want to make sure the environment variables are added properly to resources, mostly in test situations.
/// This method has asynchronous behavior when <paramref name = "applicationOperation" /> is <see cref="DistributedApplicationOperation.Run"/>
/// and environment variables were provided from <see cref="IValueProvider"/> otherwise it will be synchronous.
/// <example>
/// Using <see cref="GetEnvironmentVariableValuesAsync(IResourceWithEnvironment, DistributedApplicationOperation)"/> inside
/// a unit test to validate environment variable values.
/// <code>
/// var builder = DistributedApplication.CreateBuilder();
/// var container = builder.AddContainer("elasticsearch", "library/elasticsearch", "8.14.0")
/// .WithEnvironment("discovery.type", "single-node")
/// .WithEnvironment("xpack.security.enabled", "true");
///
/// var env = await container.Resource.GetEnvironmentVariableValuesAsync();
///
/// Assert.Collection(env,
/// env =>
/// {
/// Assert.Equal("discovery.type", env.Key);
/// Assert.Equal("single-node", env.Value);
/// },
/// env =>
/// {
/// Assert.Equal("xpack.security.enabled", env.Key);
/// Assert.Equal("true", env.Value);
/// });
/// </code>
/// </example>
/// </remarks>
[Obsolete("Use ResourceExecutionConfigurationBuilder instead.")]
public static async ValueTask<Dictionary<string, string>> GetEnvironmentVariableValuesAsync(this IResourceWithEnvironment resource,
DistributedApplicationOperation applicationOperation = DistributedApplicationOperation.Run)
{
(var executionConfiguration, _) = await resource.ExecutionConfigurationBuilder()
.WithEnvironmentVariablesConfig()
.BuildAsync(new(applicationOperation), NullLogger.Instance, CancellationToken.None).ConfigureAwait(false);
return executionConfiguration.EnvironmentVariables.ToDictionary();
}
/// <summary>
/// Get the arguments from the given resource.
/// </summary>
/// <param name="resource">The resource to get the arguments from.</param>
/// <param name="applicationOperation">The context in which the AppHost is being executed.</param>
/// <returns>The arguments retrieved from the resource.</returns>
/// <remarks>
/// This method is useful when you want to make sure the arguments are added properly to resources, mostly in test situations.
/// This method has asynchronous behavior when <paramref name = "applicationOperation" /> is <see cref="DistributedApplicationOperation.Run"/>
/// and arguments were provided from <see cref="IValueProvider"/> otherwise it will be synchronous.
/// <example>
/// Using <see cref="GetArgumentValuesAsync(IResourceWithArgs, DistributedApplicationOperation)"/> inside
/// a unit test to validate argument values.
/// <code>
/// var builder = DistributedApplication.CreateBuilder();
/// var container = builder.AddContainer("elasticsearch", "library/elasticsearch", "8.14.0")
/// .WithArgs("--discovery.type", "single-node")
/// .WithArgs("--xpack.security.enabled", "true");
///
/// var args = await container.Resource.GetArgumentsAsync();
///
/// Assert.Collection(args,
/// arg =>
/// {
/// Assert.Equal("--discovery.type", arg);
/// },
/// arg =>
/// {
/// Assert.Equal("--xpack.security.enabled", arg);
/// });
/// </code>
/// </example>
/// </remarks>
[Obsolete("Use ExecutionConfigurationBuilder instead.")]
public static async ValueTask<string[]> GetArgumentValuesAsync(this IResourceWithArgs resource,
DistributedApplicationOperation applicationOperation = DistributedApplicationOperation.Run)
{
(var argumentConfiguration, _) = await resource.ExecutionConfigurationBuilder()
.WithArgumentsConfig()
.BuildAsync(new(applicationOperation), NullLogger.Instance, CancellationToken.None).ConfigureAwait(false);
return argumentConfiguration.Arguments.Select(a => a.Value).ToArray();
}
/// <summary>
/// Gather argument values, but do not resolve them. Used to allow multiple callbacks to constructively contribute to
/// the argument list before resolving.
/// </summary>
/// <param name="resource">The resource to retrieve argument values for.</param>
/// <param name="executionContext">The execution context used during the retrieval of argument values.</param>
/// <param name="logger">The logger used for logging information or errors during the retrieval of argument values.</param>
/// <param name="cancellationToken">A token for cancelling the operation, if needed.</param>
/// <returns>A list of unprocessed argument values.</returns>
[Obsolete("Use ExecutionConfigurationBuilder instead.")]
internal static async ValueTask<List<object>> GatherArgumentValuesAsync(
this IResource resource,
DistributedApplicationExecutionContext executionContext,
ILogger logger,
CancellationToken cancellationToken = default)
{
var args = new List<object>();
if (resource.TryGetAnnotationsOfType<CommandLineArgsCallbackAnnotation>(out var callbacks))
{
var context = new CommandLineArgsCallbackContext(args, resource, cancellationToken)
{
Logger = logger,
ExecutionContext = executionContext
};
foreach (var callback in callbacks)
{
await callback.Callback(context).ConfigureAwait(false);
}
}
return args;
}
/// <summary>
/// Processes pre-gathered command-line argument values for the specified resource in the given execution context.
/// </summary>
/// <param name="resource">The resource for which the argument values are being processed.</param>
/// <param name="executionContext">The execution context used during the processing of argument values.</param>
/// <param name="arguments">The list of pre-gathered argument values to process.</param>
/// <param name="processValue">A callback invoked for each argument value, providing the unprocessed value, processed string representation, any exception, and a sensitivity flag.</param>
/// <param name="logger">The logger used for logging information or errors during the argument processing.</param>
/// <param name="cancellationToken">A token for cancelling the operation, if needed.</param>
/// <returns>A task representing the asynchronous operation.</returns>
[Obsolete("Use ExecutionConfigurationBuilder instead.")]
internal static async ValueTask ProcessGatheredArgumentValuesAsync(
this IResource resource,
DistributedApplicationExecutionContext executionContext,
List<object> arguments,
// (unprocessed, processed, exception, isSensitive)
Action<object?, string?, Exception?, bool> processValue,
ILogger logger,
CancellationToken cancellationToken = default)
{
foreach (var a in arguments)
{
try
{
var resolvedValue = await resource.ResolveValueAsync(executionContext, logger, a, null, cancellationToken).ConfigureAwait(false);
if (resolvedValue?.Value != null)
{
processValue(a, resolvedValue.Value, null, resolvedValue.IsSensitive);
}
}
catch (Exception ex)
{
processValue(a, a.ToString(), ex, false);
}
}
}
/// <summary>
/// Processes argument values for the specified resource in the given execution context.
/// </summary>
/// <param name="resource">The resource containing the argument values to process.</param>
/// <param name="executionContext">The execution context used during the processing of argument values.</param>
/// <param name="processValue">
/// A callback invoked for each argument value. This action provides the unprocessed value, processed string representation,
/// an exception if one occurs, and a boolean indicating the success of processing.
/// </param>
/// <param name="logger">The logger used for logging information or errors during the argument processing.</param>
/// <param name="cancellationToken">A token for cancelling the operation, if needed.</param>
/// <returns>A task representing the asynchronous operation.</returns>
[Obsolete("Use ExecutionConfigurationBuilder instead.")]
public static async ValueTask ProcessArgumentValuesAsync(
this IResource resource,
DistributedApplicationExecutionContext executionContext,
// (unprocessed, processed, exception, isSensitive)
Action<object?, string?, Exception?, bool> processValue,
ILogger logger,
CancellationToken cancellationToken = default)
{
var args = await GatherArgumentValuesAsync(resource, executionContext, logger, cancellationToken).ConfigureAwait(false);
await ProcessGatheredArgumentValuesAsync(resource, executionContext, args, processValue, logger, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Gather environment variable values, but do not resolve them. Used to allow multiple callbacks to
/// contribute to the environment variable list before resolving.
/// </summary>
/// <param name="resource">The resource containing the environment variables to gather.</param>
/// <param name="executionContext">The execution context used during the gathering of environment variables.</param>
/// <param name="logger">The logger used for logging information or errors during the gathering process.</param>
/// <param name="cancellationToken">A token for cancelling the operation, if needed.</param>
/// <returns>A dictionary of unprocessed environment variable values.</returns>
[Obsolete("Use ExecutionConfigurationBuilder instead.")]
internal static async ValueTask<Dictionary<string, object>> GatherEnvironmentVariableValuesAsync(
this IResource resource,
DistributedApplicationExecutionContext executionContext,
ILogger logger,
CancellationToken cancellationToken = default)
{
var config = new Dictionary<string, object>();
if (resource.TryGetEnvironmentVariables(out var callbacks))
{
var context = new EnvironmentCallbackContext(executionContext, resource, config, cancellationToken)
{
Logger = logger
};
foreach (var callback in callbacks)
{
await callback.Callback(context).ConfigureAwait(false);
}
}
return config;
}
/// <summary>
/// Processes pre-gathered environment variable values for the specified resource within the given execution context.
/// </summary>
/// <param name="resource">The resource for which the environment variables are being processed.</param>
/// <param name="executionContext">The execution context used during the processing of environment variables.</param>
/// <param name="environmentVariables">The pre-gathered environment variable values to be processed.</param>
/// <param name="processValue">An action delegate invoked for each environment variable, providing the key, the unprocessed value, the processed value (if available), and any exception encountered during processing.</param>
/// <param name="logger">The logger used to log any information or errors during the environment variables processing.</param>
/// <param name="cancellationToken">A cancellation token to observe during the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
[Obsolete("Use ExecutionConfigurationBuilder instead.")]
internal static async ValueTask ProcessGatheredEnvironmentVariableValuesAsync(
this IResource resource,
DistributedApplicationExecutionContext executionContext,
Dictionary<string, object> environmentVariables,
Action<string, object?, string?, Exception?> processValue,
ILogger logger,
CancellationToken cancellationToken = default)
{
foreach (var (key, expr) in environmentVariables)
{
try
{
var resolvedValue = await resource.ResolveValueAsync(executionContext, logger, expr, key, cancellationToken).ConfigureAwait(false);
if (resolvedValue?.Value is not null)
{
processValue(key, expr, resolvedValue.Value, null);
}
}
catch (Exception ex)
{
processValue(key, expr, expr?.ToString(), ex);
}
}
}
/// <summary>
/// Processes environment variable values for the specified resource within the given execution context.
/// </summary>
/// <param name="resource">The resource from which the environment variables are retrieved and processed.</param>
/// <param name="executionContext">The execution context to be used for processing the environment variables.</param>
/// <param name="processValue">An action delegate invoked for each environment variable, providing the key, the unprocessed value, the processed value (if available), and any exception encountered during processing.</param>
/// <param name="logger">The logger used to log any information or errors during the environment variables processing.</param>
/// <param name="cancellationToken">A cancellation token to observe during the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
[Obsolete("Use ExecutionConfigurationBuilder instead.")]
public static async ValueTask ProcessEnvironmentVariableValuesAsync(
this IResource resource,
DistributedApplicationExecutionContext executionContext,
Action<string, object?, string?, Exception?> processValue,
ILogger logger,
CancellationToken cancellationToken = default)
{
var config = await GatherEnvironmentVariableValuesAsync(resource, executionContext, logger, cancellationToken).ConfigureAwait(false);
await ProcessGatheredEnvironmentVariableValuesAsync(resource, executionContext, config, processValue, logger, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Processes all container build options callback annotations on a resource by invoking them in order.
/// </summary>
/// <param name="resource">The resource to process container build options for.</param>
/// <param name="serviceProvider">The service provider for dependency injection.</param>
/// <param name="logger">The logger used to log any information or errors during processing.</param>
/// <param name="executionContext">The optional execution context.</param>
/// <param name="cancellationToken">A cancellation token to observe during the asynchronous operation.</param>
/// <returns>A context object containing the accumulated container build options from all callbacks.</returns>
[Experimental("ASPIRECOMPUTE001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
internal static async ValueTask<ContainerBuildOptionsCallbackContext> ProcessContainerBuildOptionsCallbackAsync(
this IResource resource,
IServiceProvider serviceProvider,
ILogger logger,
DistributedApplicationExecutionContext? executionContext = null,
CancellationToken cancellationToken = default)
{
var context = new ContainerBuildOptionsCallbackContext(
resource,
serviceProvider,
logger,
cancellationToken,
executionContext);
if (resource.TryGetAnnotationsOfType<ContainerBuildOptionsCallbackAnnotation>(out var annotations))
{
foreach (var annotation in annotations)
{
await annotation.Callback(context).ConfigureAwait(false);
}
}
return context;
}
/// <summary>
/// Configures container build options for a compute resource using a callback.
/// </summary>
/// <typeparam name="T">The resource type.</typeparam>
/// <param name="builder">The resource builder.</param>
/// <param name="callback">A callback to configure container build options.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
[Experimental("ASPIRECOMPUTE001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public static IResourceBuilder<T> WithContainerBuildOptions<T>(
this IResourceBuilder<T> builder,
Action<ContainerBuildOptionsCallbackContext> callback)
where T : IResource, IComputeResource
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(callback);
return builder.WithAnnotation(new ContainerBuildOptionsCallbackAnnotation(callback), ResourceAnnotationMutationBehavior.Append);
}
/// <summary>
/// Configures container build options for a compute resource using an async callback.
/// </summary>
/// <typeparam name="T">The resource type.</typeparam>
/// <param name="builder">The resource builder.</param>
/// <param name="callback">An async callback to configure container build options.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
[Experimental("ASPIRECOMPUTE001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public static IResourceBuilder<T> WithContainerBuildOptions<T>(
this IResourceBuilder<T> builder,
Func<ContainerBuildOptionsCallbackContext, Task> callback)
where T : IResource, IComputeResource
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(callback);
return builder.WithAnnotation(new ContainerBuildOptionsCallbackAnnotation(callback), ResourceAnnotationMutationBehavior.Append);
}
internal static NetworkIdentifier GetDefaultResourceNetwork(this IResource resource)
{
return resource.IsContainer() ? KnownNetworkIdentifiers.DefaultAspireContainerNetwork : KnownNetworkIdentifiers.LocalhostNetwork;
}
internal static IEnumerable<NetworkIdentifier> GetSupportedNetworks(this IResource resource)
{
return resource.IsContainer() ? [KnownNetworkIdentifiers.DefaultAspireContainerNetwork, KnownNetworkIdentifiers.LocalhostNetwork] : [KnownNetworkIdentifiers.LocalhostNetwork];
}
internal static async ValueTask<ResolvedValue?> ResolveValueAsync(
this IResource resource,
DistributedApplicationExecutionContext executionContext,
ILogger logger,
object? value,
string? key = null,
CancellationToken cancellationToken = default)
{
return (executionContext.Operation, value) switch
{
(_, string s) => new(s, false),
(DistributedApplicationOperation.Run, IValueProvider provider) => await resource.GetValue(executionContext, key, provider, logger, cancellationToken).ConfigureAwait(false),
(DistributedApplicationOperation.Run, IResourceBuilder<IResource> rb) when rb.Resource is IValueProvider provider => await resource.GetValue(executionContext, key, provider, logger, cancellationToken).ConfigureAwait(false),
(DistributedApplicationOperation.Publish, IManifestExpressionProvider provider) => new(provider.ValueExpression, false),
(DistributedApplicationOperation.Publish, IResourceBuilder<IResource> rb) when rb.Resource is IManifestExpressionProvider provider => new(provider.ValueExpression, false),
(_, { } o) => new(o.ToString(), false),
(_, null) => new(null, false),
};
}
/// <summary>
/// Gets a value indicating whether the resource is excluded from being published.
/// </summary>
/// <param name="resource">The resource to determine if it should be excluded from being published.</param>
public static bool IsExcludedFromPublish(this IResource resource) =>
resource.TryGetLastAnnotation<ManifestPublishingCallbackAnnotation>(out var lastAnnotation) && lastAnnotation == ManifestPublishingCallbackAnnotation.Ignore;
internal static async ValueTask ProcessContainerRuntimeArgValues(
this IResource resource,
DistributedApplicationExecutionContext executionContext,
Action<string?, Exception?> processValue,
ILogger logger,
CancellationToken cancellationToken = default)
{
// Apply optional extra arguments to the container run command.
if (resource.TryGetAnnotationsOfType<ContainerRuntimeArgsCallbackAnnotation>(out var runArgsCallback))
{
var args = new List<object>();
var containerRunArgsContext = new ContainerRuntimeArgsCallbackContext(args, cancellationToken);
foreach (var callback in runArgsCallback)
{
await callback.Callback(containerRunArgsContext).ConfigureAwait(false);
}
foreach (var arg in args)
{
try
{
var value = arg switch
{
string s => s,
IValueProvider valueProvider => (await resource.GetValue(executionContext, key: null, valueProvider, logger, cancellationToken).ConfigureAwait(false))?.Value,
{ } obj => obj.ToString(),
null => null
};
if (value is not null)
{
processValue(value, null);
}
}
catch (Exception ex)
{
processValue(arg.ToString(), ex);
}
}
}
}
private static async Task<ResolvedValue?> GetValue(this IResource resource, DistributedApplicationExecutionContext executionContext, string? key, IValueProvider valueProvider, ILogger logger, CancellationToken cancellationToken)
{
var task = ExpressionResolver.ResolveAsync(valueProvider, new ValueProviderContext() { ExecutionContext = executionContext, Caller = resource }, cancellationToken);
if (!task.IsCompleted)
{
if (valueProvider is IResource providerResource)
{
if (key is null)
{
logger.LogInformation("Waiting for value from resource '{ResourceName}'", providerResource.Name);
}
else
{
logger.LogInformation("Waiting for value for environment variable value '{Name}' from resource '{ResourceName}'", key, providerResource.Name);
}
}
else if (valueProvider is ConnectionStringReference { Resource: var cs })
{
logger.LogInformation("Waiting for value for connection string from resource '{ResourceName}'", cs.Name);
}
else
{
if (key is null)
{
logger.LogInformation("Waiting for value from {ValueProvider}.", valueProvider.ToString());
}
else
{
logger.LogInformation("Waiting for value for environment variable value '{Name}' from {ValueProvider}.", key, valueProvider.ToString());
}
}
}
return await task.ConfigureAwait(false);
}
/// <summary>
/// Attempts to get the container mounts for the specified resource.
/// </summary>
/// <param name="resource">The resource to get the volume mounts for.</param>
/// <param name="volumeMounts">When this method returns, contains the volume mounts for the specified resource, if found; otherwise, <c>null</c>.</param>
/// <returns><c>true</c> if the volume mounts were successfully retrieved; otherwise, <c>false</c>.</returns>
public static bool TryGetContainerMounts(this IResource resource, [NotNullWhen(true)] out IEnumerable<ContainerMountAnnotation>? volumeMounts)
{
return TryGetAnnotationsOfType<ContainerMountAnnotation>(resource, out volumeMounts);
}
/// <summary>
/// Attempts to retrieve the endpoints for the given resource.
/// </summary>
/// <param name="resource">The resource to retrieve the endpoints for.</param>
/// <param name="endpoints">The endpoints for the given resource, if found.</param>
/// <returns>True if the endpoints were found, false otherwise.</returns>
public static bool TryGetEndpoints(this IResource resource, [NotNullWhen(true)] out IEnumerable<EndpointAnnotation>? endpoints)
{
return TryGetAnnotationsOfType(resource, out endpoints);
}
/// <summary>
/// Attempts to retrieve the URLs for the given resource.
/// </summary>
/// <param name="resource">The resource to retrieve the URLs for.</param>
/// <param name="urls">The URLs for the given resource, if found.</param>
/// <returns>True if the URLs were found, false otherwise.</returns>
public static bool TryGetUrls(this IResource resource, [NotNullWhen(true)] out IEnumerable<ResourceUrlAnnotation>? urls)
{
return TryGetAnnotationsOfType(resource, out urls);
}
/// <summary>
/// Gets references to all endpoints for the specified resource.
/// </summary>
/// <param name="resource">The <see cref="IResourceWithEndpoints"/> which contains <see cref="EndpointAnnotation"/> annotations.</param>
/// <returns>An enumeration of <see cref="EndpointReference"/> based on the <see cref="EndpointAnnotation"/> annotations from the resources' <see cref="IResource.Annotations"/> collection.</returns>
public static IEnumerable<EndpointReference> GetEndpoints(this IResourceWithEndpoints resource)
{
if (TryGetAnnotationsOfType<EndpointAnnotation>(resource, out var endpoints))
{
return endpoints.Select(e => new EndpointReference(resource, e));
}
return [];
}
/// <summary>
/// Gets references to all endpoints for the specified resource.
/// </summary>
/// <param name="resource">The <see cref="IResourceWithEndpoints"/> which contains <see cref="EndpointAnnotation"/> annotations.</param>
/// <param name="contextNetworkID">The ID of the network that serves as the context context for the endpoint references.</param>
/// <returns>An enumeration of <see cref="EndpointReference"/> based on the <see cref="EndpointAnnotation"/> annotations from the resources' <see cref="IResource.Annotations"/> collection.</returns>
public static IEnumerable<EndpointReference> GetEndpoints(this IResourceWithEndpoints resource, NetworkIdentifier contextNetworkID)
{
if (TryGetAnnotationsOfType<EndpointAnnotation>(resource, out var endpoints))
{
return endpoints.Select(e => new EndpointReference(resource, e, contextNetworkID));
}
return [];
}
/// <summary>
/// Gets an endpoint reference for the specified endpoint name.
/// </summary>
/// <param name="resource">The <see cref="IResourceWithEndpoints"/> which contains <see cref="EndpointAnnotation"/> annotations.</param>
/// <param name="endpointName">The name of the endpoint.</param>
/// <returns>An <see cref="EndpointReference"/>object providing resolvable reference for the specified endpoint.</returns>
public static EndpointReference GetEndpoint(this IResourceWithEndpoints resource, string endpointName)
{
var endpoint = resource.TryGetEndpoints(out var endpoints) ?
endpoints.FirstOrDefault(e => StringComparers.EndpointAnnotationName.Equals(e.Name, endpointName)) :
null;
if (endpoint is null)
{
return new EndpointReference(resource, endpointName);
}
else
{
return new EndpointReference(resource, endpoint);
}
}
/// <summary>
/// Gets an endpoint reference for the specified endpoint name.
/// </summary>
/// <param name="resource">The <see cref="IResourceWithEndpoints"/> which contains <see cref="EndpointAnnotation"/> annotations.</param>
/// <param name="endpointName">The name of the endpoint.</param>
/// <param name="contextNetworkID">The network ID of the network that provides the context for the returned <see cref="EndpointReference"/></param>
/// <returns>An <see cref="EndpointReference"/>object providing resolvable reference for the specified endpoint.</returns>
public static EndpointReference GetEndpoint(this IResourceWithEndpoints resource, string endpointName, NetworkIdentifier contextNetworkID)
{
var endpoint = resource.TryGetEndpoints(out var endpoints) ?
endpoints.FirstOrDefault(e => StringComparers.EndpointAnnotationName.Equals(e.Name, endpointName)) :
null;
if (endpoint is null)
{
return new EndpointReference(resource, endpointName, contextNetworkID);
}
else
{
return new EndpointReference(resource, endpoint, contextNetworkID);
}
}
/// <summary>
/// Resolves endpoint port configuration for the specified resource.
/// Computes target ports and exposed ports based on resource type, endpoint configuration,
/// and whether the endpoint is considered a default HTTP endpoint.
/// </summary>
/// <param name="resource">The resource containing endpoints to resolve.</param>
/// <param name="portAllocator">Optional port allocator. If null, uses default allocation starting from port 8000.</param>
/// <returns>A read-only list of resolved endpoints with computed port values.</returns>
public static IReadOnlyList<ResolvedEndpoint> ResolveEndpoints(this IResource resource, IPortAllocator? portAllocator = null)
{
if (!resource.TryGetEndpoints(out var endpoints))
{
return [];
}
portAllocator ??= new PortAllocator();
var httpSchemesEncountered = new HashSet<string>();
var result = new List<ResolvedEndpoint>();
foreach (var endpoint in endpoints)
{
// Compute target port based on resource type and endpoint configuration
ResolvedPort targetPort = (resource, endpoint.UriScheme, endpoint.TargetPort, endpoint.Port) switch
{
// The port was explicitly specified so use it
(_, _, int target, _) => ResolvedPort.Explicit(target),
// Container resources get their default listening port from the exposed port (implicit)
(ContainerResource, _, null, int port) => ResolvedPort.Implicit(port),
// Check whether the project views this endpoint as Default (for its scheme).
// If so, we don't specify the target port, as it will get one from the deployment tool.
(ProjectResource, string uriScheme, null, _) when IsHttpScheme(uriScheme) && !httpSchemesEncountered.Contains(uriScheme) => ResolvedPort.None(),
// Allocate a dynamic port
_ => ResolvedPort.Allocated(portAllocator.AllocatePort())
};
// Track HTTP schemes encountered for ProjectResources
if (resource is ProjectResource && IsHttpScheme(endpoint.UriScheme))
{
httpSchemesEncountered.Add(endpoint.UriScheme);
}
// Compute exposed port (host port)
ResolvedPort exposedPort = (endpoint.UriScheme, endpoint.Port, targetPort.Value) switch
{
// Port set explicitly, use it
(_, int port, _) => ResolvedPort.Explicit(port),
// We have a target port, infer the exposedPort from it
(_, null, int targetPortValue) => ResolvedPort.Implicit(targetPortValue),
// Let the tool infer the default http and https ports
("http", null, null) => ResolvedPort.None(),
("https", null, null) => ResolvedPort.None(),
// Other schemes just allocate a port
_ => ResolvedPort.Allocated(portAllocator.AllocatePort())
};
// Track used ports to avoid collisions when allocating
if (exposedPort.Value is int ep)
{
portAllocator.AddUsedPort(ep);
}
if (targetPort.Value is int tp)
{
portAllocator.AddUsedPort(tp);
}
result.Add(new ResolvedEndpoint
{
Endpoint = endpoint,
TargetPort = targetPort,
ExposedPort = exposedPort
});
}
return result;
static bool IsHttpScheme(string scheme) => scheme is "http" or "https";
}
/// <summary>
/// Attempts to get the container image name from the given resource.
/// </summary>
/// <param name="resource">The resource to get the container image name from.</param>
/// <param name="imageName">The container image name if found, otherwise null.</param>
/// <returns>True if the container image name was found, otherwise false.</returns>
public static bool TryGetContainerImageName(this IResource resource, [NotNullWhen(true)] out string? imageName)
{
return TryGetContainerImageName(resource, useBuiltImage: true, out imageName);
}
/// <summary>
/// Attempts to get the container image name from the given resource.
/// </summary>
/// <param name="resource">The resource to get the container image name from.</param>
/// <param name="useBuiltImage">When true, uses the image name from DockerfileBuildAnnotation if present. When false, uses only ContainerImageAnnotation.</param>
/// <param name="imageName">The container image name if found, otherwise null.</param>
/// <returns>True if the container image name was found, otherwise false.</returns>
public static bool TryGetContainerImageName(this IResource resource, bool useBuiltImage, [NotNullWhen(true)] out string? imageName)
{
// First check if there's a DockerfileBuildAnnotation with an image name/tag
// This takes precedence over the ContainerImageAnnotation when building from a Dockerfile
if (useBuiltImage &&
resource.Annotations.OfType<DockerfileBuildAnnotation>().SingleOrDefault() is { } buildAnnotation &&
!string.IsNullOrEmpty(buildAnnotation.ImageName))
{
var tagSuffix = string.IsNullOrEmpty(buildAnnotation.ImageTag) ? string.Empty : $":{buildAnnotation.ImageTag}";
imageName = $"{buildAnnotation.ImageName}{tagSuffix}";
return true;
}
if (resource.Annotations.OfType<ContainerImageAnnotation>().LastOrDefault() is { } imageAnnotation)
{
var registryPrefix = string.IsNullOrEmpty(imageAnnotation.Registry) ? string.Empty : $"{imageAnnotation.Registry}/";
if (string.IsNullOrEmpty(imageAnnotation.SHA256))
{
var tagSuffix = string.IsNullOrEmpty(imageAnnotation.Tag) ? string.Empty : $":{imageAnnotation.Tag}";
imageName = $"{registryPrefix}{imageAnnotation.Image}{tagSuffix}";
}
else
{
var shaSuffix = $"@sha256:{imageAnnotation.SHA256}";
imageName = $"{registryPrefix}{imageAnnotation.Image}{shaSuffix}";
}
return true;
}
imageName = null;
return false;
}
/// <summary>
/// Gets the number of replicas for the specified resource. Defaults to <c>1</c> if no
/// <see cref="ReplicaAnnotation" /> is found.
/// </summary>
/// <param name="resource">The resource to get the replica count for.</param>
/// <returns>The number of replicas for the specified resource.</returns>
public static int GetReplicaCount(this IResource resource)
{
if (resource.TryGetLastAnnotation<ReplicaAnnotation>(out var replicaAnnotation))
{
return replicaAnnotation.Replicas;
}
else
{
return 1;
}
}
/// <summary>
/// Determines whether the specified resource requires image building.
/// </summary>
/// <remarks>
/// Resources require an image build if they provide their own Dockerfile or are a project.
/// </remarks>
/// <param name="resource">The resource to evaluate for image build requirements.</param>
/// <returns>True if the resource requires image building; otherwise, false.</returns>
public static bool RequiresImageBuild(this IResource resource)
{
return resource is ProjectResource || resource.TryGetLastAnnotation<DockerfileBuildAnnotation>(out _);
}
/// <summary>
/// Determines whether the specified resource requires image building and pushing.
/// </summary>
/// <remarks>
/// Resources require an image build and a push to a container registry if they provide
/// their own Dockerfile or are a project.
/// </remarks>
/// <param name="resource">The resource to evaluate for image push requirements.</param>
/// <returns>True if the resource requires image building and pushing; otherwise, false.</returns>
public static bool RequiresImageBuildAndPush(this IResource resource)
{
return resource.RequiresImageBuild() && !resource.IsBuildOnlyContainer();
}
internal static bool IsBuildOnlyContainer(this IResource resource)
{
return resource.TryGetLastAnnotation<DockerfileBuildAnnotation>(out var dockerfileBuild) &&
!dockerfileBuild.HasEntrypoint;
}
/// <summary>
/// Gets the deployment target for the specified resource, if any. Throws an exception if
/// there are multiple compute environments and a compute environment is not explicitly specified.
/// </summary>
public static DeploymentTargetAnnotation? GetDeploymentTargetAnnotation(this IResource resource, IComputeEnvironmentResource? targetComputeEnvironment = null)
{
IComputeEnvironmentResource? selectedComputeEnvironment = null;
if (resource.TryGetLastAnnotation<ComputeEnvironmentAnnotation>(out var computeEnvironmentAnnotation))
{
// If you have a ComputeEnvironmentAnnotation, it means the resource is bound to a specific compute environment.
// Skip the annotation if it doesn't match the specified computeEnvironmentResource.
if (targetComputeEnvironment is not null && targetComputeEnvironment != computeEnvironmentAnnotation.ComputeEnvironment)
{
return null;
}
// If the resource is bound to a specific compute environment, use that one.
selectedComputeEnvironment = computeEnvironmentAnnotation.ComputeEnvironment;
}
if (resource.TryGetAnnotationsOfType<DeploymentTargetAnnotation>(out var deploymentTargetAnnotations))
{
var annotations = deploymentTargetAnnotations.ToArray();
if (selectedComputeEnvironment is not null)
{
return annotations.SingleOrDefault(a => a.ComputeEnvironment == selectedComputeEnvironment);
}
if (annotations.Length > 1)
{
var computeEnvironmentNames = string.Join(", ", annotations.Select(a => a.ComputeEnvironment?.Name));
throw new InvalidOperationException($"Resource '{resource.Name}' has multiple compute environments - '{computeEnvironmentNames}'. Please specify a single compute environment using 'WithComputeEnvironment'.");
}
return annotations[0];
}
return null;
}
/// <summary>
/// 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 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.
/// </returns>
internal static ContainerLifetime GetContainerLifetimeType(this IResource resource)
{
if (resource.TryGetLastAnnotation<ContainerLifetimeAnnotation>(out var lifetimeAnnotation))
{
return lifetimeAnnotation.Lifetime;
}
return ContainerLifetime.Session;
}
/// <summary>
/// Determines whether the specified resource has a pull policy annotation and retrieves the value if it does.
/// </summary>
/// <param name="resource">The resource to check for a ContainerPullPolicy annotation</param>
/// <param name="pullPolicy">The <see cref="ImagePullPolicy"/> for the annotation</param>
/// <returns>True if an annotation exists, false otherwise</returns>
internal static bool TryGetContainerImagePullPolicy(this IResource resource, [NotNullWhen(true)] out ImagePullPolicy? pullPolicy)
{
if (resource.TryGetLastAnnotation<ContainerImagePullPolicyAnnotation>(out var pullPolicyAnnotation))
{
pullPolicy = pullPolicyAnnotation.ImagePullPolicy;
return true;
}
pullPolicy = null;
return false;
}
/// <summary>
/// Determines whether a resource has proxy support enabled or not. Container resources may have a <see cref="ProxySupportAnnotation"/> setting that disables proxying for their
/// endpoints regardless of the endpoint proxy configuration.
/// </summary>
/// <param name="resource">The resource to get proxy support for.</param>
/// <returns>True if the resource supports proxied endpoints/services, false otherwise.</returns>
internal static bool SupportsProxy(this IResource resource)
{
// If the resource doesn't have a ProxySupportAnnotation or the ProxyEnabled property on the annotation is true, then the resource supports proxying.
return !resource.TryGetLastAnnotation<ProxySupportAnnotation>(out var proxySupportAnnotation) || proxySupportAnnotation.ProxyEnabled;
}
/// <summary>
/// Get the top resource in the resource hierarchy.
/// e.g. for a AzureBlobStorageResource, the top resource is the AzureStorageResource.
/// </summary>
internal static IResource GetRootResource(this IResource resource) =>
resource switch
{
IResourceWithParent resWithParent => resWithParent.Parent.GetRootResource(),
_ => resource
};
/// <summary>
/// Returns a single DCP resource name for the specified resource.
/// Throws <see cref="InvalidOperationException"/> if the resource has no resolved names or multiple resolved names.
/// </summary>
internal static string GetResolvedResourceName(this IResource resource)
{
var names = resource.GetResolvedResourceNames();
if (names.Length == 0)
{
throw new InvalidOperationException($"Resource '{resource.Name}' has no resolved names.");
}
if (names.Length > 1)
{
throw new InvalidOperationException($"Resource '{resource.Name}' has multiple resolved names: {string.Join(", ", names)}.");
}
return names[0];
}
/// <summary>
/// Gets resolved names for the specified resource.
/// DCP resources are given a unique suffix as part of the complete name. We want to use that value.
/// Also, a DCP resource could have multiple instances. All instance names are returned for a resource.
/// </summary>
internal static string[] GetResolvedResourceNames(this IResource resource)
{
if (resource.TryGetLastAnnotation<DcpInstancesAnnotation>(out var replicaAnnotation) && !replicaAnnotation.Instances.IsEmpty)
{
return replicaAnnotation.Instances.Select(i => i.Name).ToArray();
}
else
{
return [resource.Name];
}
}
/// <summary>
/// Processes image push options callbacks for the specified resource.
/// </summary>
/// <param name="resource">The resource to process image push options for.</param>
/// <param name="cancellationToken">A cancellation token to observe while processing.</param>
/// <returns>The resolved image push options.</returns>
internal static async Task<ContainerImagePushOptions> ProcessImagePushOptionsCallbackAsync(
this IResource resource,
CancellationToken cancellationToken)
{
var options = new ContainerImagePushOptions
{
RemoteImageName = resource.Name.ToLowerInvariant(),
RemoteImageTag = "latest"
};
var context = new ContainerImagePushOptionsCallbackContext
{
Resource = resource,
CancellationToken = cancellationToken,
Options = options
};
var callbacks = resource.Annotations.OfType<ContainerImagePushOptionsCallbackAnnotation>();
foreach (var callback in callbacks)
{
await callback.Callback(context).ConfigureAwait(false);
}
return options;
}
/// <summary>
/// Gets the container registry associated with the specified resource.
/// </summary>
/// <param name="resource">The resource to get the container registry for.</param>
/// <returns>The container registry associated with the resource.</returns>
/// <exception cref="InvalidOperationException">Thrown when the resource does not have a container registry reference.</exception>
/// <remarks>
/// This method checks for a container registry in the following order:
/// <list type="number">
/// <item>The <see cref="DeploymentTargetAnnotation"/> on the resource.</item>
/// <item>The <see cref="ContainerRegistryReferenceAnnotation"/> on the resource (set via <c>WithContainerRegistry</c>).</item>
/// <item>The <see cref="RegistryTargetAnnotation"/> on the resource (automatically added when a registry is added to the app model).</item>
/// </list>
/// </remarks>
internal static IContainerRegistry GetContainerRegistry(this IResource resource)
{
// Try to get the container registry from DeploymentTargetAnnotation first
var deploymentTarget = resource.GetDeploymentTargetAnnotation();
if (deploymentTarget?.ContainerRegistry is not null)
{
return deploymentTarget.ContainerRegistry;
}
// Try ContainerRegistryReferenceAnnotation (explicit WithContainerRegistry call)
var registryAnnotation = resource.Annotations.OfType<ContainerRegistryReferenceAnnotation>().LastOrDefault();
if (registryAnnotation is not null)
{
return registryAnnotation.Registry;
}
// Fall back to RegistryTargetAnnotation (added automatically via BeforeStartEvent)
var registryTargetAnnotations = resource.Annotations.OfType<RegistryTargetAnnotation>().ToArray();
if (registryTargetAnnotations.Length == 1)
{
return registryTargetAnnotations[0].Registry;
}
if (registryTargetAnnotations.Length > 1)
{
var registryNames = string.Join(", ", registryTargetAnnotations.Select(a => a.Registry is IResource res ? res.Name : a.Registry.ToString()));
throw new InvalidOperationException(
$"Resource '{resource.Name}' has multiple container registries available - '{registryNames}'. " +
$"Please specify which registry to use with '.WithContainerRegistry(registryBuilder)'.");
}
throw new InvalidOperationException($"Resource '{resource.Name}' does not have a container registry reference.");
}
/// <summary>
/// Gets the full remote image name for the specified resource, including registry endpoint and tag.
/// </summary>
/// <param name="resource">The resource to get the remote image name for.</param>
/// <param name="cancellationToken">A cancellation token to observe while processing.</param>
/// <returns>The fully qualified remote image name.</returns>
/// <exception cref="InvalidOperationException">Thrown when the resource does not have a container registry reference.</exception>
/// <remarks>
/// This method processes any image push options callbacks on the resource and combines the result
/// with the container registry to produce the full remote image name.
/// </remarks>
internal static async Task<string> GetFullRemoteImageNameAsync(
this IResource resource,
CancellationToken cancellationToken)
{
var pushOptions = await resource.ProcessImagePushOptionsCallbackAsync(cancellationToken).ConfigureAwait(false);
var registry = resource.GetContainerRegistry();
return await pushOptions.GetFullRemoteImageNameAsync(registry, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Gets a logger for the specified resource using the provided service provider.
/// </summary>
/// <param name="resource">The resource to get the logger for.</param>
/// <param name="serviceProvider">The service provider to resolve dependencies.</param>
/// <returns>A logger instance for the specified resource.</returns>
internal static ILogger GetLogger(this IResource resource, IServiceProvider serviceProvider)
{
var resourceLoggerService = serviceProvider.GetRequiredService<ResourceLoggerService>();
return resourceLoggerService.GetLogger(resource);
}
}
|