|
#pragma warning disable ASPIREPUBLISHERS001
#pragma warning disable ASPIRECOMPUTE001
#pragma warning disable ASPIREAZURE001
#pragma warning disable ASPIREPIPELINES001
using System.Diagnostics;
using System.IO.Compression;
using System.Net.Http.Headers;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure;
using Aspire.Hosting.Pipelines;
using Aspire.Hosting.Publishing;
using Azure.Core;
using Azure.Identity;
using Azure.Provisioning;
namespace Pipelines.Library;
public static class DistributedApplicationPipelineExtensions
{
public static IDistributedApplicationPipeline AddAppServiceZipDeploy(this IDistributedApplicationPipeline pipeline)
{
pipeline.AddStep("app-service-zip-deploy", async context =>
{
var appServiceEnvironments = context.Model.Resources.OfType<AzureAppServiceEnvironmentResource>();
if (!appServiceEnvironments.Any())
{
return;
}
foreach (var appServiceEnvironment in appServiceEnvironments)
{
foreach (var resource in context.Model.GetComputeResources())
{
var annotation = resource.GetDeploymentTargetAnnotation();
if (annotation != null &&
annotation.ComputeEnvironment == appServiceEnvironment &&
annotation.DeploymentTarget is AzureAppServiceWebSiteResource websiteResource)
{
if (resource is not ProjectResource projectResource)
{
continue;
}
await DeployProjectToAppServiceAsync(
context,
projectResource,
websiteResource,
appServiceEnvironment,
context.CancellationToken).ConfigureAwait(false);
}
}
}
}, dependsOn: WellKnownPipelineSteps.DeployCompute);
return pipeline;
}
private static async Task DeployProjectToAppServiceAsync(
DeployingContext context,
ProjectResource projectResource,
AzureAppServiceWebSiteResource websiteResource,
AzureAppServiceEnvironmentResource appServiceEnvironment,
CancellationToken cancellationToken)
{
var stepName = $"deploy-{projectResource.Name}";
var step = await context.ActivityReporter.CreateStepAsync(stepName, cancellationToken).ConfigureAwait(false);
await using (step.ConfigureAwait(false))
{
var projectMetadata = projectResource.GetProjectMetadata();
var projectPath = projectMetadata.ProjectPath;
var publishTask = await step.CreateTaskAsync($"Publishing {projectResource.Name}", cancellationToken).ConfigureAwait(false);
await using (publishTask.ConfigureAwait(false))
{
var publishDir = Path.Combine(Path.GetTempPath(), $"aspire-publish-{Guid.NewGuid()}");
Directory.CreateDirectory(publishDir);
try
{
var publishProcess = Process.Start(new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"publish \"{projectPath}\" -c Release -o \"{publishDir}\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
});
if (publishProcess == null)
{
await publishTask.CompleteAsync(
"Failed to start dotnet publish",
CompletionState.CompletedWithError,
cancellationToken).ConfigureAwait(false);
return;
}
await publishProcess.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
if (publishProcess.ExitCode != 0)
{
var error = await publishProcess.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
await publishTask.CompleteAsync(
$"Publish failed: {error}",
CompletionState.CompletedWithError,
cancellationToken).ConfigureAwait(false);
return;
}
await publishTask.CompleteAsync(
"Publish completed",
CompletionState.Completed,
cancellationToken).ConfigureAwait(false);
var zipTask = await step.CreateTaskAsync($"Creating deployment package", cancellationToken).ConfigureAwait(false);
await using (zipTask.ConfigureAwait(false))
{
var zipPath = Path.Combine(Path.GetTempPath(), $"aspire-deploy-{Guid.NewGuid()}.zip");
ZipFile.CreateFromDirectory(publishDir, zipPath);
await zipTask.CompleteAsync(
"Deployment package created",
CompletionState.Completed,
cancellationToken).ConfigureAwait(false);
var uploadTask = await step.CreateTaskAsync($"Uploading to {projectResource.Name}", cancellationToken).ConfigureAwait(false);
await using (uploadTask.ConfigureAwait(false))
{
try
{
var siteName = websiteResource.Outputs[$"{Infrastructure.NormalizeBicepIdentifier(websiteResource.Name)}_name"]?.ToString();
if (string.IsNullOrEmpty(siteName))
{
siteName = appServiceEnvironment.Outputs["name"]?.ToString();
}
if (string.IsNullOrEmpty(siteName))
{
await uploadTask.CompleteAsync(
"Could not determine website name",
CompletionState.CompletedWithError,
cancellationToken).ConfigureAwait(false);
return;
}
var credential = new AzureCliCredential();
var tokenRequestContext = new TokenRequestContext(["https://management.azure.com/.default"]);
var accessToken = await credential.GetTokenAsync(tokenRequestContext, cancellationToken).ConfigureAwait(false);
var kuduUrl = $"https://{siteName}.scm.azurewebsites.net/api/zipdeploy";
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Token);
httpClient.Timeout = TimeSpan.FromMinutes(30);
await using var zipStream = File.OpenRead(zipPath);
using var content = new StreamContent(zipStream);
content.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
var response = await httpClient.PostAsync(kuduUrl, content, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
await uploadTask.CompleteAsync(
$"Upload failed: {response.StatusCode} - {errorContent}",
CompletionState.CompletedWithError,
cancellationToken).ConfigureAwait(false);
return;
}
await uploadTask.CompleteAsync(
"Upload completed successfully",
CompletionState.Completed,
cancellationToken).ConfigureAwait(false);
}
finally
{
if (File.Exists(zipPath))
{
File.Delete(zipPath);
}
}
}
}
}
finally
{
if (Directory.Exists(publishDir))
{
Directory.Delete(publishDir, recursive: true);
}
}
}
await step.CompleteAsync(
"Deployment completed",
CompletionState.Completed,
cancellationToken).ConfigureAwait(false);
}
}
}
|