|
#pragma warning disable ASPIREINTERACTION001
// 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 Aspire.Dashboard.Model;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Resources;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.SecretManager.Tools.Internal;
namespace Aspire.Hosting;
/// <summary>
/// Handles processing of parameter resources during application orchestration.
/// </summary>
[Experimental("ASPIREINTERACTION001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public sealed class ParameterProcessor(
ResourceNotificationService notificationService,
ResourceLoggerService loggerService,
IInteractionService interactionService,
ILogger<ParameterProcessor> logger,
DistributedApplicationOptions options,
DistributedApplicationExecutionContext executionContext)
{
private readonly List<ParameterResource> _unresolvedParameters = [];
/// <summary>
/// Initializes parameter resources and handles unresolved parameters if interaction service is available.
/// </summary>
/// <param name="parameterResources">The parameter resources to initialize.</param>
/// <param name="waitForResolution">Whether to wait for all parameters to be resolved before completing the returned Task.</param>
/// <returns>A task that completes when all parameters are resolved (if waitForResolution is true) or when initialization is complete.</returns>
public async Task InitializeParametersAsync(IEnumerable<ParameterResource> parameterResources, bool waitForResolution = false)
{
// Initialize all parameter resources by setting their WaitForValueTcs.
// This allows them to be processed asynchronously later.
foreach (var parameterResource in parameterResources)
{
parameterResource.WaitForValueTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
await ProcessParameterAsync(parameterResource).ConfigureAwait(false);
}
// If interaction service is available, we can handle unresolved parameters.
// This will allow the user to provide values for parameters that could not be initialized.
if (interactionService.IsAvailable && _unresolvedParameters.Count > 0)
{
// Start the loop that will allow the user to specify values for unresolved parameters.
var parameterResolutionTask = Task.Run(async () =>
{
try
{
await HandleUnresolvedParametersAsync().ConfigureAwait(false);
logger.LogDebug("All unresolved parameters have been handled successfully.");
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to handle unresolved parameters.");
}
});
if (waitForResolution)
{
await parameterResolutionTask.ConfigureAwait(false);
}
}
}
/// <summary>
/// Initializes parameter resources by collecting dependent parameters from the distributed application model
/// and handles unresolved parameters if interaction service is available.
/// </summary>
/// <param name="model">The distributed application model to collect parameters from.</param>
/// <param name="waitForResolution">Whether to wait for all parameters to be resolved before completing the returned Task.</param>
/// <param name="cancellationToken">The cancellation token to observe while waiting for parameters to be resolved.</param>
/// <returns>A task that completes when all parameters are resolved (if waitForResolution is true) or when initialization is complete.</returns>
public async Task InitializeParametersAsync(DistributedApplicationModel model, bool waitForResolution = false, CancellationToken cancellationToken = default)
{
var referencedParameters = new Dictionary<string, ParameterResource>();
var currentDependencySet = new HashSet<object?>();
await CollectDependentParameterResourcesAsync(model, referencedParameters, currentDependencySet, cancellationToken).ConfigureAwait(false);
// Combine explicit parameters with dependent parameters
var explicitParameters = model.Resources.OfType<ParameterResource>();
var dependentParameters = referencedParameters.Values.Where(p => !explicitParameters.Contains(p));
var allParameters = explicitParameters.Concat(dependentParameters);
if (allParameters.Any())
{
await InitializeParametersAsync(allParameters, waitForResolution).ConfigureAwait(false);
}
}
private async Task CollectDependentParameterResourcesAsync(DistributedApplicationModel model, Dictionary<string, ParameterResource> referencedParameters, HashSet<object?> currentDependencySet, CancellationToken cancellationToken)
{
foreach (var resource in model.Resources)
{
if (resource.IsExcludedFromPublish())
{
continue;
}
await ProcessResourceDependenciesAsync(resource, executionContext, referencedParameters, currentDependencySet, cancellationToken).ConfigureAwait(false);
}
}
private async Task ProcessResourceDependenciesAsync(IResource resource, DistributedApplicationExecutionContext executionContext, Dictionary<string, ParameterResource> referencedParameters, HashSet<object?> currentDependencySet, CancellationToken cancellationToken)
{
// Process environment variables
await resource.ProcessEnvironmentVariableValuesAsync(
executionContext,
(key, unprocessed, processed, ex) =>
{
if (unprocessed is not null)
{
TryAddDependentParameters(unprocessed, referencedParameters, currentDependencySet);
}
},
logger,
cancellationToken: cancellationToken).ConfigureAwait(false);
// Process command line arguments
await resource.ProcessArgumentValuesAsync(
executionContext,
(unprocessed, expression, ex, _) =>
{
if (unprocessed is not null)
{
TryAddDependentParameters(unprocessed, referencedParameters, currentDependencySet);
}
},
logger,
cancellationToken: cancellationToken).ConfigureAwait(false);
}
private static void TryAddDependentParameters(object? value, Dictionary<string, ParameterResource> referencedParameters, HashSet<object?> currentDependencySet)
{
if (value is ParameterResource parameter)
{
referencedParameters.TryAdd(parameter.Name, parameter);
}
else if (value is IValueWithReferences objectWithReferences)
{
currentDependencySet.Add(value);
foreach (var dependency in objectWithReferences.References)
{
if (!currentDependencySet.Contains(dependency))
{
TryAddDependentParameters(dependency, referencedParameters, currentDependencySet);
}
}
currentDependencySet.Remove(value);
}
}
private async Task ProcessParameterAsync(ParameterResource parameterResource)
{
try
{
var value = parameterResource.ValueInternal ?? "";
// Check if we need to validate GenerateParameterDefault in publish mode
// We use GetParameterValue to distinguish between configured values and generated values
// because ValueInternal might contain a generated value even if no configuration was provided.
if (parameterResource.Default is GenerateParameterDefault generateDefault && executionContext.IsPublishMode)
{
// Try to get a configured value (without using the default) to see if the parameter was actually specified. This will throw if the value is missing.
var configuration = executionContext.ServiceProvider.GetRequiredService<IConfiguration>();
value = ParameterResourceBuilderExtensions.GetParameterValue(configuration, parameterResource.Name, parameterDefault: null, parameterResource.ConfigurationKey);
}
await notificationService.PublishUpdateAsync(parameterResource, s =>
{
return s with
{
Properties = s.Properties.SetResourceProperty(KnownProperties.Parameter.Value, value, parameterResource.Secret),
State = KnownResourceStates.Running
};
})
.ConfigureAwait(false);
parameterResource.WaitForValueTcs?.TrySetResult(value);
}
catch (Exception ex)
{
// Missing parameter values throw a MissingParameterValueException.
if (interactionService.IsAvailable && ex is MissingParameterValueException)
{
// If interaction service is available, we can prompt the user to provide a value.
// Add the parameter to unresolved parameters list.
_unresolvedParameters.Add(parameterResource);
loggerService.GetLogger(parameterResource)
.LogWarning("Parameter resource {ResourceName} could not be initialized. Waiting for user input.", parameterResource.Name);
}
else
{
// If interaction service is not available, we log the error and set the state to error.
parameterResource.WaitForValueTcs?.TrySetException(ex);
loggerService.GetLogger(parameterResource)
.LogError(ex, "Failed to initialize parameter resource {ResourceName}.", parameterResource.Name);
}
var stateText = ex is MissingParameterValueException ?
"Value missing" :
"Error initializing parameter";
await notificationService.PublishUpdateAsync(parameterResource, s =>
{
return s with
{
State = new(stateText, KnownResourceStateStyles.Error),
Properties = s.Properties.SetResourceProperty(KnownProperties.Parameter.Value, ex.Message)
};
})
.ConfigureAwait(false);
}
}
// Internal for testing purposes.
private async Task HandleUnresolvedParametersAsync()
{
await HandleUnresolvedParametersAsync(_unresolvedParameters).ConfigureAwait(false);
}
// Internal for testing purposes - allows passing specific parameters to test.
internal async Task HandleUnresolvedParametersAsync(IList<ParameterResource> unresolvedParameters)
{
// This method will continue in a loop until all unresolved parameters are resolved.
while (unresolvedParameters.Count > 0)
{
var showNotification = true;
var showSaveToSecrets = true;
// Skip notification and save to user secrets prompts during publish mode
if (executionContext.IsPublishMode)
{
showNotification = false;
showSaveToSecrets = false;
}
var proceedToInputs = true;
if (showNotification)
{
// First we show a notification that there are unresolved parameters.
var result = await interactionService.PromptNotificationAsync(
InteractionStrings.ParametersBarTitle,
InteractionStrings.ParametersBarMessage,
new NotificationInteractionOptions
{
Intent = MessageIntent.Warning,
PrimaryButtonText = InteractionStrings.ParametersBarPrimaryButtonText
})
.ConfigureAwait(false);
proceedToInputs = result.Data;
}
if (proceedToInputs)
{
// Now we build up a new form base on the unresolved parameters.
var resourceInputs = new List<(ParameterResource ParameterResource, InteractionInput Input)>();
foreach (var parameter in unresolvedParameters)
{
// Create an input for each unresolved parameter.
var input = parameter.CreateInput();
resourceInputs.Add((parameter, input));
}
var inputs = resourceInputs.Select(i => i.Input).ToList();
InteractionInput? saveParameters = null;
if (showSaveToSecrets)
{
saveParameters = new InteractionInput
{
Name = "RememberParameters",
InputType = InputType.Boolean,
Label = InteractionStrings.ParametersInputsRememberLabel
};
inputs.Add(saveParameters);
}
var message = executionContext.IsPublishMode
? InteractionStrings.ParametersInputsMessagePublishMode
: InteractionStrings.ParametersInputsMessage;
var valuesPrompt = await interactionService.PromptInputsAsync(
InteractionStrings.ParametersInputsTitle,
message,
[.. inputs],
new InputsDialogInteractionOptions
{
PrimaryButtonText = InteractionStrings.ParametersInputsPrimaryButtonText,
ShowDismiss = true,
EnableMessageMarkdown = true,
})
.ConfigureAwait(false);
if (!valuesPrompt.Canceled)
{
// Iterate through the unresolved parameters and set their values based on user input.
for (var i = resourceInputs.Count - 1; i >= 0; i--)
{
var (parameter, input) = (resourceInputs[i].ParameterResource, resourceInputs[i].Input);
var inputValue = input.Value;
if (string.IsNullOrEmpty(inputValue))
{
// If the input value is null, we skip this parameter.
continue;
}
parameter.WaitForValueTcs?.TrySetResult(inputValue);
// Update the parameter resource state to active with the provided value.
await notificationService.PublishUpdateAsync(parameter, s =>
{
return s with
{
Properties = s.Properties.SetResourceProperty(KnownProperties.Parameter.Value, inputValue, parameter.Secret),
State = KnownResourceStates.Running
};
})
.ConfigureAwait(false);
// Log that the parameter has been resolved
loggerService.GetLogger(parameter)
.LogInformation("Parameter resource {ResourceName} has been resolved via user interaction.", parameter.Name);
// Persist the parameter value to user secrets if requested.
if (showSaveToSecrets &&
saveParameters != null &&
bool.TryParse(saveParameters.Value, out var saveToSecrets) &&
saveToSecrets)
{
SecretsStore.TrySetUserSecret(options.Assembly, parameter.ConfigurationKey, inputValue);
}
// Remove the parameter from unresolved parameters list.
unresolvedParameters.RemoveAt(i);
}
}
}
}
}
}
|