|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#pragma warning disable ASPIREPUBLISHERS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Aspire.Hosting.Publishing;
internal class Publisher(
IPublishingActivityReporter progressReporter,
ILogger<Publisher> logger,
IOptions<PublishingOptions> options,
DistributedApplicationExecutionContext executionContext,
IServiceProvider serviceProvider) : IDistributedApplicationPublisher
{
public async Task PublishAsync(DistributedApplicationModel model, CancellationToken cancellationToken)
{
if (options.Value.OutputPath == null && !options.Value.Deploy)
{
throw new DistributedApplicationException(
"The '--output-path [path]' option was not specified."
);
}
// Check if --clear-cache flag is set and prompt user before deleting deployment state
if (options.Value.Deploy && options.Value.ClearCache)
{
var deploymentStateManager = serviceProvider.GetService<IDeploymentStateManager>();
if (deploymentStateManager?.StateFilePath is not null && File.Exists(deploymentStateManager.StateFilePath))
{
var interactionService = serviceProvider.GetService<IInteractionService>();
if (interactionService?.IsAvailable == true)
{
var hostEnvironment = serviceProvider.GetService<Microsoft.Extensions.Hosting.IHostEnvironment>();
var environmentName = hostEnvironment?.EnvironmentName ?? "Production";
var result = await interactionService.PromptNotificationAsync(
"Clear Deployment State",
$"The deployment state for the '{environmentName}' environment will be deleted. All Azure resources will be re-provisioned. Do you want to continue?",
new NotificationInteractionOptions
{
Intent = MessageIntent.Confirmation,
ShowSecondaryButton = true,
ShowDismiss = false,
PrimaryButtonText = "Yes",
SecondaryButtonText = "No"
},
cancellationToken).ConfigureAwait(false);
if (result.Canceled || !result.Data)
{
// User declined or canceled - exit the deployment
logger.LogInformation("User declined to clear deployment state. Canceling deployment.");
return;
}
// User confirmed - delete the deployment state file
logger.LogInformation("Deleting deployment state file at {Path} due to --clear-cache flag", deploymentStateManager.StateFilePath);
File.Delete(deploymentStateManager.StateFilePath);
}
}
}
// Add a step to do model analysis before publishing/deploying
var step = await progressReporter.CreateStepAsync(
"Analyzing model.",
cancellationToken).ConfigureAwait(false);
await using (step.ConfigureAwait(false))
{
var task = await step.CreateTaskAsync(
"Analyzing the distributed application model for publishing and deployment capabilities.",
cancellationToken)
.ConfigureAwait(false);
var targetResources = new List<IResource>();
foreach (var resource in model.Resources)
{
if (options.Value.Deploy)
{
if (resource.HasAnnotationOfType<DeployingCallbackAnnotation>())
{
targetResources.Add(resource);
}
}
else
{
if (resource.HasAnnotationOfType<PublishingCallbackAnnotation>())
{
targetResources.Add(resource);
}
}
}
var (message, state) = GetTaskInfo(targetResources, options.Value.Deploy);
await task.CompleteAsync(
message,
state,
cancellationToken)
.ConfigureAwait(false);
// Add a task to show the deployment state file path if available
if (options.Value.Deploy && !options.Value.ClearCache)
{
var deploymentStateManager = serviceProvider.GetService<IDeploymentStateManager>();
if (deploymentStateManager?.StateFilePath is not null && File.Exists(deploymentStateManager.StateFilePath))
{
var statePathTask = await step.CreateTaskAsync(
"Checking deployment state configuration.",
cancellationToken)
.ConfigureAwait(false);
await statePathTask.CompleteAsync(
$"Deployment state will be loaded from: {deploymentStateManager.StateFilePath}",
CompletionState.Completed,
cancellationToken)
.ConfigureAwait(false);
}
}
if (state == CompletionState.CompletedWithError)
{
// If there are no resources to publish or deploy, we can exit early
return;
}
}
// If deployment is enabled, run deploying callbacks after publishing
if (options.Value.Deploy)
{
// Initialize parameters as a pre-requisite for deployment
var parameterProcessor = serviceProvider.GetRequiredService<ParameterProcessor>();
await parameterProcessor.InitializeParametersAsync(model, waitForResolution: true, cancellationToken).ConfigureAwait(false);
var deployingContext = new DeployingContext(model, executionContext, serviceProvider, logger, cancellationToken, options.Value.OutputPath is not null ?
Path.GetFullPath(options.Value.OutputPath) : null);
await deployingContext.WriteModelAsync(model).ConfigureAwait(false);
}
else
{
var outputPath = Path.GetFullPath(options.Value.OutputPath!);
var publishingContext = new PublishingContext(model, executionContext, serviceProvider, logger, cancellationToken, outputPath);
await publishingContext.WriteModelAsync(model).ConfigureAwait(false);
}
}
private static (string Message, CompletionState State) GetTaskInfo(List<IResource> targetResources, bool isDeploy)
{
var operation = isDeploy ? "deployment" : "publishing";
return targetResources.Count switch
{
0 => ($"No resources in the distributed application model support {operation}.", CompletionState.CompletedWithError),
_ => ($"Found {targetResources.Count} resources that support {operation}. ({string.Join(", ", targetResources.Select(r => r.GetType().Name))})", CompletionState.Completed)
};
}
}
|