Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
8 changes: 4 additions & 4 deletions Aspire.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@
<Project Path="playground/AspireWithJavaScript/AspireJavaScript.MinimalApi/AspireJavaScript.MinimalApi.csproj" />
<Project Path="playground/AspireWithJavaScript/AspireJavaScript.ServiceDefaults/AspireJavaScript.ServiceDefaults.csproj" />
</Folder>
<Folder Name="/playground/AspireWithPython/">
<Project Path="playground/AspireWithPython/AspireWithPython.AppHost/AspireWithPython.AppHost.csproj" />
<Project Path="playground/AspireWithPython/AspireWithPython.ServiceDefaults/AspireWithPython.ServiceDefaults.csproj" />
</Folder>
<Folder Name="/playground/AzureAIFoundryEndToEnd/">
<Project Path="playground/AzureAIFoundryEndToEnd/AzureAIFoundryEndToEnd.AppHost/AzureAIFoundryEndToEnd.AppHost.csproj" />
<Project Path="playground/AzureAIFoundryEndToEnd/AzureAIFoundryEndToEnd.WebStory/AzureAIFoundryEndToEnd.WebStory.csproj" />
Expand Down Expand Up @@ -278,10 +282,6 @@
<Folder Name="/playground/python/">
<Project Path="playground/python/Python.AppHost/Python.AppHost.csproj" />
</Folder>
<Folder Name="/playground/pythonweb/">
<Project Path="playground/AspireWithPython/AspireWithPython.AppHost/AspireWithPython.AppHost.csproj" />
<Project Path="playground/AspireWithPython/AspireWithPython.ServiceDefaults/AspireWithPython.ServiceDefaults.csproj" />
</Folder>
<Folder Name="/playground/qdrant/">
<Project Path="playground/Qdrant/Qdrant.ApiService/Qdrant.ApiService.csproj" />
<Project Path="playground/Qdrant/Qdrant.AppHost/Qdrant.AppHost.csproj" />
Expand Down
4 changes: 2 additions & 2 deletions playground/pipelines/Pipelines.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ await assignRoleTask.CompleteAsync(
context.CancellationToken).ConfigureAwait(false);
}
}
}, requiredBy: "upload-bind-mounts", dependsOn: WellKnownPipelineSteps.ProvisionInfrastructure);
}, requiredBy: "upload-bind-mounts", dependsOn: WellKnownPipelineTags.ProvisionInfrastructure);

builder.Pipeline.AddStep("upload-bind-mounts", async (deployingContext) =>
{
Expand Down Expand Up @@ -324,7 +324,7 @@ await uploadTask.CompleteAsync(
totalUploads += fileCount;
}
}
}, requiredBy: WellKnownPipelineSteps.DeployCompute, dependsOn: WellKnownPipelineSteps.ProvisionInfrastructure);
}, requiredBy: WellKnownPipelineTags.DeployCompute, dependsOn: WellKnownPipelineTags.ProvisionInfrastructure);

builder.AddProject<Projects.Publishers_ApiService>("api-service")
.WithComputeEnvironment(aasEnv)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ await DeployProjectToAppServiceAsync(
}
}
}
}, dependsOn: WellKnownPipelineSteps.DeployCompute);
}, dependsOn: WellKnownPipelineTags.DeployCompute);

return pipeline;
}
Expand Down
97 changes: 73 additions & 24 deletions src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ namespace Aspire.Hosting.Azure;
[Experimental("ASPIREAZURE001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")]
public sealed class AzureEnvironmentResource : Resource
{
private const string DefaultImageStepTag = "default-image-tags";

/// <summary>
/// Gets or sets the Azure location that the resources will be deployed to.
/// </summary>
Expand All @@ -46,6 +48,8 @@ public sealed class AzureEnvironmentResource : Resource
/// </summary>
public ParameterResource PrincipalId { get; set; }

private readonly List<IResource> _computeResourcesToBuild = [];

/// <summary>
/// Initializes a new instance of the <see cref="AzureEnvironmentResource"/> class.
/// </summary>
Expand Down Expand Up @@ -82,16 +86,25 @@ public AzureEnvironmentResource(string name, ParameterResource location, Paramet

var provisionStep = new PipelineStep
{
Name = WellKnownPipelineSteps.ProvisionInfrastructure,
Name = WellKnownPipelineTags.ProvisionInfrastructure,
Action = ctx => ProvisionAzureBicepResourcesAsync(ctx, provisioningContext!)
};
provisionStep.DependsOn(createContextStep);

var addImageTagsStep = new PipelineStep
{
Name = DefaultImageStepTag,
Action = ctx => DefaultImageTags(ctx),
Tags = [DefaultImageStepTag],
};

var buildStep = new PipelineStep
{
Name = WellKnownPipelineSteps.BuildCompute,
Action = ctx => BuildContainerImagesAsync(ctx)
Name = WellKnownPipelineTags.BuildCompute,
Action = ctx => BuildContainerImagesAsync(ctx),
Tags = [WellKnownPipelineTags.BuildCompute],
};
buildStep.DependsOn(addImageTagsStep);

var pushStep = new PipelineStep
{
Expand All @@ -103,7 +116,7 @@ public AzureEnvironmentResource(string name, ParameterResource location, Paramet

var deployStep = new PipelineStep
{
Name = WellKnownPipelineSteps.DeployCompute,
Name = WellKnownPipelineTags.DeployCompute,
Action = ctx => DeployComputeResourcesAsync(ctx, provisioningContext!)
};
deployStep.DependsOn(pushStep);
Expand All @@ -116,7 +129,38 @@ public AzureEnvironmentResource(string name, ParameterResource location, Paramet
};
printDashboardUrlStep.DependsOn(deployStep);

return [validateStep, createContextStep, provisionStep, buildStep, pushStep, deployStep, printDashboardUrlStep];
return [validateStep, createContextStep, provisionStep, addImageTagsStep, buildStep, pushStep, deployStep, printDashboardUrlStep];
}));

Annotations.Add(new PipelineConfigurationAnnotation(context =>
{
var defaultImageTags = context.FindStepsByTagAndResource(DefaultImageStepTag, this).Single();
var myBuildStep = context.FindStepsByTagAndResource(WellKnownPipelineTags.BuildCompute, this).Single();

var computeResources = context.ApplicationModel.Resources
.Where(r => r.RequiresImageBuild())
.ToList();

foreach (var computeResource in computeResources)
{
var computeResourceBuildSteps = context.FindStepsByTagAndResource(WellKnownPipelineTags.BuildCompute, computeResource);
if (computeResourceBuildSteps.Any())
{
// add the appropriate dependencies to the compute resource build steps
foreach (var computeBuildStep in computeResourceBuildSteps)
{
computeBuildStep.DependsOn(defaultImageTags);
myBuildStep.DependsOn(computeBuildStep);
}
}
else
{
// No build step exists for this compute resource, so we add it to the main build step
_computeResourcesToBuild.Add(computeResource);
}
}

return Task.CompletedTask;
}));

Annotations.Add(ManifestPublishingCallbackAnnotation.Ignore);
Expand All @@ -126,6 +170,26 @@ public AzureEnvironmentResource(string name, ParameterResource location, Paramet
PrincipalId = principalId;
}

private static Task DefaultImageTags(PipelineStepContext context)
{
var computeResources = context.Model.Resources
.Where(r => r.RequiresImageBuild())
.ToList();

var deploymentTag = $"aspire-deploy-{DateTime.UtcNow:yyyyMMddHHmmss}";
foreach (var resource in computeResources)
{
if (resource.TryGetLastAnnotation<DeploymentImageTagCallbackAnnotation>(out _))
{
continue;
}
resource.Annotations.Add(
new DeploymentImageTagCallbackAnnotation(_ => deploymentTag));
}

return Task.CompletedTask;
}

private Task PublishAsync(PublishingContext context)
{
var azureProvisioningOptions = context.Services.GetRequiredService<IOptions<AzureProvisioningOptions>>();
Expand Down Expand Up @@ -240,32 +304,17 @@ await resourceTask.CompleteAsync(
await Task.WhenAll(provisioningTasks).ConfigureAwait(false);
}

private static async Task BuildContainerImagesAsync(PipelineStepContext context)
private async Task BuildContainerImagesAsync(PipelineStepContext context)
{
var containerImageBuilder = context.Services.GetRequiredService<IResourceContainerImageBuilder>();

var computeResources = context.Model.GetComputeResources()
.Where(r => r.RequiresImageBuildAndPush())
.ToList();

if (!computeResources.Any())
if (!_computeResourcesToBuild.Any())
{
return;
}

var deploymentTag = $"aspire-deploy-{DateTime.UtcNow:yyyyMMddHHmmss}";
foreach (var resource in computeResources)
{
if (resource.TryGetLastAnnotation<DeploymentImageTagCallbackAnnotation>(out _))
{
continue;
}
resource.Annotations.Add(
new DeploymentImageTagCallbackAnnotation(_ => deploymentTag));
}
var containerImageBuilder = context.Services.GetRequiredService<IResourceContainerImageBuilder>();

await containerImageBuilder.BuildImagesAsync(
computeResources,
_computeResourcesToBuild,
new ContainerBuildOptions
{
TargetPlatform = ContainerTargetPlatform.LinuxAmd64
Expand Down
8 changes: 7 additions & 1 deletion src/Aspire.Hosting.NodeJs/NodeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,13 @@ public static IResourceBuilder<ViteAppResource> AddViteApp(this IDistributedAppl
.Run($"{resourceBuilder.Resource.Command} {string.Join(' ', packageManagerAnnotation.BuildCommandLineArgs)}");
}
});
});

// since Vite apps are typically served via a separate web server, we don't set an entrypoint
var dockerFileAnnotation = resource.Annotations.OfType<DockerfileBuildAnnotation>().LastOrDefault()
?? throw new InvalidOperationException("DockerfileBuildAnnotation should after calling PublishAsDockerFile.");
dockerFileAnnotation.HasEntrypoint = false;
})
.WithAnnotation(new StaticDockerFilesAnnotation() { SourcePath = "/app/dist" });
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting.NodeJs/ViteAppResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ namespace Aspire.Hosting.NodeJs;
/// <param name="command">The command to execute the Vite application, such as the script or entry point.</param>
/// <param name="workingDirectory">The working directory from which the Vite application command is executed.</param>
public class ViteAppResource(string name, string command, string workingDirectory)
: NodeAppResource(name, command, workingDirectory);
: NodeAppResource(name, command, workingDirectory), IResourceWithStaticDockerFiles;
112 changes: 112 additions & 0 deletions src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.ApplicationModel.Docker;
using Aspire.Hosting.Pipelines;
using Aspire.Hosting.Publishing;
using Aspire.Hosting.Python;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -233,6 +236,44 @@ public static IResourceBuilder<PythonAppResource> AddPythonApp(
.WithArgs(scriptArgs);
}

/// <summary>
/// Adds a Uvicorn-based Python application to the distributed application builder with HTTP endpoint configuration.
/// </summary>
/// <remarks>This method configures the application to use Uvicorn as the server and exposes an HTTP
/// endpoint. When publishing, it sets the entry point to use the Uvicorn executable with appropriate arguments for
/// host and port.</remarks>
/// <param name="builder">The distributed application builder to which the Uvicorn application resource will be added.</param>
/// <param name="name">The unique name of the Uvicorn application resource.</param>
/// <param name="appDirectory">The directory containing the Python application files.</param>
/// <param name="moduleName"></param>
/// <returns>A resource builder for further configuration of the Uvicorn Python application resource.</returns>
public static IResourceBuilder<PythonAppResource> AddUvicornApp(
this IDistributedApplicationBuilder builder, [ResourceName] string name, string appDirectory, string moduleName)
{
var resourceBuilder = builder.AddPythonExecutable(name, appDirectory, "uvicorn")
.WithHttpEndpoint(env: "PORT")
.WithArgs(c =>
{
c.Args.Add(moduleName);

c.Args.Add("--host");
var endpoint = ((IResourceWithEndpoints)c.Resource).GetEndpoint("http");
if (builder.ExecutionContext.IsPublishMode)
{
c.Args.Add("0.0.0.0");
}
else
{
c.Args.Add(endpoint.EndpointAnnotation.TargetHost);
}

c.Args.Add("--port");
c.Args.Add(endpoint.Property(EndpointProperty.TargetPort));
});

return resourceBuilder;
}

private static IResourceBuilder<PythonAppResource> AddPythonAppCore(
IDistributedApplicationBuilder builder, string name, string appDirectory, EntrypointType entrypointType,
string entrypoint, string virtualEnvironmentPath)
Expand Down Expand Up @@ -465,6 +506,7 @@ private static IResourceBuilder<PythonAppResource> AddPythonAppCore(
var runtimeBuilder = context.Builder
.From($"python:{pythonVersion}-slim-bookworm", "app")
.EmptyLine()
.AddStaticFiles(context.Resource, "/app")
.Comment("------------------------------")
.Comment("🚀 Runtime stage")
.Comment("------------------------------")
Expand Down Expand Up @@ -504,9 +546,79 @@ private static IResourceBuilder<PythonAppResource> AddPythonAppCore(
});
});

resourceBuilder.WithPipelineStepFactory(factoryContext =>
{
var buildStep = new PipelineStep
{
Name = $"{factoryContext.Resource.Name}-build-compute",
Action = async ctx =>
{
var containerImageBuilder = ctx.Services.GetRequiredService<IResourceContainerImageBuilder>();

// ensure any static file references' images are built first
if (factoryContext.Resource.TryGetAnnotationsOfType<StaticDockerFileDestinationAnnotation>(out var staticFileAnnotations))
{
foreach (var staticFileAnnotation in staticFileAnnotations)
{
await containerImageBuilder.BuildImageAsync(
staticFileAnnotation.Source,
new ContainerBuildOptions
{
TargetPlatform = ContainerTargetPlatform.LinuxAmd64
},
ctx.CancellationToken).ConfigureAwait(false);
}
}

await containerImageBuilder.BuildImageAsync(
factoryContext.Resource,
new ContainerBuildOptions
{
TargetPlatform = ContainerTargetPlatform.LinuxAmd64
},
ctx.CancellationToken).ConfigureAwait(false);
},
Tags = [WellKnownPipelineTags.BuildCompute]
};

return buildStep;
})
.WithAnnotation(new PipelineBuildComputeStepAnnotation() { StepName = $"{resource.Name}-build-compute" });

return resourceBuilder;
}

private static DockerfileStage AddStaticFiles(this DockerfileStage stage, IResource resource, string rootDestinationPath)
{
if (resource.TryGetAnnotationsOfType<StaticDockerFileDestinationAnnotation>(out var staticFileDestinationAnnotations))
{
foreach (var staticFileDestAnnotation in staticFileDestinationAnnotations)
{
// get image name
if (!staticFileDestAnnotation.Source.TryGetContainerImageName(out var imageName))
{
throw new InvalidOperationException("Cannot add static files: Source resource does not have a container image name.");
}

// get the source path
if (!staticFileDestAnnotation.Source.TryGetLastAnnotation<StaticDockerFilesAnnotation>(out var staticFileAnnotation))
{
throw new InvalidOperationException("Cannot add static files: Source resource does not have a static file source path annotation.");
}

var destinationPath = staticFileDestAnnotation.DestinationPath;
if (!destinationPath.StartsWith('/'))
{
destinationPath = $"{rootDestinationPath}/{destinationPath}";
}
stage.CopyFrom(imageName, staticFileAnnotation.SourcePath, destinationPath);
}

stage.EmptyLine();
}
return stage;
}

private static void ThrowIfNullOrContainsIsNullOrEmpty(string[] scriptArgs)
{
ArgumentNullException.ThrowIfNull(scriptArgs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public static IEnumerable<IResource> GetComputeResources(this DistributedApplica
continue;
}

if (r.IsBuildOnlyContainer())
{
continue;
}

yield return r;
}
}
Expand Down
Loading
Loading