Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move PublishAsDockerFile to use container resources. #7072

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class DockerfileBuildAnnotation(string contextPath, string dockerfilePath
/// <summary>
/// Gets the arguments to pass to the build.
/// </summary>
public Dictionary<string, object> BuildArguments { get; } = [];
public Dictionary<string, object?> BuildArguments { get; } = [];

/// <summary>
/// Gets the secrets to pass to the build.
Expand Down
3 changes: 1 addition & 2 deletions src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -464,11 +464,10 @@ public static IResourceBuilder<T> WithContainerName<T>(this IResourceBuilder<T>
/// builder.Build().Run();
/// </code>
/// </example>
public static IResourceBuilder<T> WithBuildArg<T>(this IResourceBuilder<T> builder, string name, object value) where T : ContainerResource
public static IResourceBuilder<T> WithBuildArg<T>(this IResourceBuilder<T> builder, string name, object? value) where T : ContainerResource
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrEmpty(name);
ArgumentNullException.ThrowIfNull(value);

var annotation = builder.Resource.Annotations.OfType<DockerfileBuildAnnotation>().SingleOrDefault();

Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Hosting/Dcp/ApplicationExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1781,6 +1781,7 @@ private static async Task ApplyBuildArgumentsAsync(Container dcpContainerResourc
string stringValue => stringValue,
IValueProvider valueProvider => await valueProvider.GetValueAsync(cancellationToken).ConfigureAwait(false),
bool boolValue => boolValue ? "true" : "false",
null => null,
_ => buildArgument.Value.ToString()
};

Expand Down
76 changes: 60 additions & 16 deletions src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Publishing;
using Aspire.Hosting.Utils;

namespace Aspire.Hosting;
Expand Down Expand Up @@ -60,6 +59,17 @@ public static IResourceBuilder<ExecutableResource> AddExecutable(this IDistribut
});
}

/// <summary>
/// Adds annotation to <see cref="ExecutableResource" /> to support containerization during deployment.
/// </summary>
/// <typeparam name="T">Type of executable resource</typeparam>
/// <param name="builder">Resource builder</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<T> PublishAsDockerFile<T>(this IResourceBuilder<T> builder) where T : ExecutableResource
{
return builder.PublishAsDockerFile(c => { });
}

/// <summary>
/// Adds annotation to <see cref="ExecutableResource" /> to support containerization during deployment.
/// The resulting container image is built, and when the optional <paramref name="buildArgs"/> are provided
Expand All @@ -69,30 +79,64 @@ public static IResourceBuilder<ExecutableResource> AddExecutable(this IDistribut
/// <param name="builder">Resource builder</param>
/// <param name="buildArgs">The optional build arguments, used with <c>docker build --build-args</c>.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<T> PublishAsDockerFile<T>(this IResourceBuilder<T> builder, IEnumerable<DockerBuildArg>? buildArgs = null) where T : ExecutableResource
public static IResourceBuilder<T> PublishAsDockerFile<T>(this IResourceBuilder<T> builder, IEnumerable<DockerBuildArg>? buildArgs) where T : ExecutableResource
{
ArgumentNullException.ThrowIfNull(builder);

return builder.WithManifestPublishingCallback(context => WriteExecutableAsDockerfileResourceAsync(context, builder.Resource, buildArgs));
return builder.PublishAsDockerFile(c =>
{
foreach (var arg in buildArgs ?? [])
{
c.WithBuildArg(arg.Name, arg.Value);
}
});
}

private static async Task WriteExecutableAsDockerfileResourceAsync(ManifestPublishingContext context, ExecutableResource executable, IEnumerable<DockerBuildArg>? buildArgs = null)
/// <summary>
/// Adds support for containerizating this <see cref="ExecutableResource"/> during deployment.
/// The resulting container image is built, and when the optional <paramref name="configure"/> action is provided,
/// it is used to configure the container resource.
/// </summary>
/// <typeparam name="T">Type of executable resource</typeparam>
/// <param name="builder">Resource builder</param>
/// <param name="configure">Optional action to configure the container resource</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<T> PublishAsDockerFile<T>(this IResourceBuilder<T> builder, Action<IResourceBuilder<ContainerResource>>? configure)
where T : ExecutableResource
{
context.Writer.WriteString("type", "dockerfile.v0");
if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
{
return builder;
}

var appHostRelativePathToDockerfile = Path.Combine(executable.WorkingDirectory, "Dockerfile");
var manifestFileRelativePathToDockerfile = context.GetManifestRelativePath(appHostRelativePathToDockerfile);
context.Writer.WriteString("path", manifestFileRelativePathToDockerfile);
// The implementation here is less than ideal, but we don't have a clean way of building resource types
// that change their behavior based on the context. In this case, we want to change the behavior of the
// resource from an ExecutableResource to a ContainerResource. We do this by removing the ExecutableResource
// from the application model and adding a new ContainerResource in its place in publish mode.

var manifestFileRelativePathToContextDirectory = context.GetManifestRelativePath(executable.WorkingDirectory);
context.Writer.WriteString("context", manifestFileRelativePathToContextDirectory);
// There are still dangling references to the original ExecutableResource in the application model, but
// in publish mode, it won't be used. This is a limitation of the current design.
builder.ApplicationBuilder.Resources.Remove(builder.Resource);

if (buildArgs is not null)
{
context.WriteDockerBuildArgs(buildArgs);
}
var container = new ExecutableContainerResource(builder.Resource);
var cb = builder.ApplicationBuilder.AddResource(container);
cb.WithImage(builder.Resource.Name);
cb.WithDockerfile(contextPath: builder.Resource.WorkingDirectory);
// Clear the runtime args
cb.WithArgs(c => c.Args.Clear());

configure?.Invoke(cb);

await context.WriteEnvironmentVariablesAsync(executable).ConfigureAwait(false);
context.WriteBindings(executable);
// Even through we're adding a ContainerResource
// update the manifest publishing callback on the original ExecutableResource
// so that the container resource is written to the manifest
return builder.WithManifestPublishingCallback(context =>
context.WriteContainerAsync(container));
}

// Allows us to mirror annotations from ExecutableResource to ContainerResource
private sealed class ExecutableContainerResource(ExecutableResource er) : ContainerResource(er.Name)
{
public override ResourceAnnotationCollection Annotations => er.Annotations;
}
}
1 change: 0 additions & 1 deletion src/Aspire.Hosting/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,6 @@ static Aspire.Hosting.DistributedApplication.CreateBuilder() -> Aspire.Hosting.I
static Aspire.Hosting.DistributedApplication.CreateBuilder(Aspire.Hosting.DistributedApplicationOptions! options) -> Aspire.Hosting.IDistributedApplicationBuilder!
static Aspire.Hosting.DistributedApplication.CreateBuilder(string![]! args) -> Aspire.Hosting.IDistributedApplicationBuilder!
static Aspire.Hosting.ExecutableResourceBuilderExtensions.AddExecutable(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string! command, string! workingDirectory, params string![]? args) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ExecutableResource!>!
static Aspire.Hosting.ExecutableResourceBuilderExtensions.PublishAsDockerFile<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, System.Collections.Generic.IEnumerable<Aspire.Hosting.ApplicationModel.DockerBuildArg!>? buildArgs = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ExecutableResourceExtensions.GetExecutableResources(this Aspire.Hosting.ApplicationModel.DistributedApplicationModel! model) -> System.Collections.Generic.IEnumerable<Aspire.Hosting.ApplicationModel.ExecutableResource!>!
static Aspire.Hosting.Lifecycle.LifecycleHookServiceCollectionExtensions.AddLifecycleHook<T>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> void
static Aspire.Hosting.Lifecycle.LifecycleHookServiceCollectionExtensions.AddLifecycleHook<T>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Func<System.IServiceProvider!, T!>! implementationFactory) -> void
Expand Down
7 changes: 5 additions & 2 deletions src/Aspire.Hosting/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.HealthStatus.get -> Micro
Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.Relationships.get -> System.Collections.Immutable.ImmutableArray<Aspire.Hosting.ApplicationModel.RelationshipSnapshot!>
Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.Relationships.init -> void
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.BuildArguments.get -> System.Collections.Generic.Dictionary<string!, object!>!
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.BuildArguments.get -> System.Collections.Generic.Dictionary<string!, object?>!
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.BuildSecrets.get -> System.Collections.Generic.Dictionary<string!, object!>!
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.ContextPath.get -> string!
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.DockerfileBuildAnnotation(string! contextPath, string! dockerfilePath, string? stage) -> void
Expand Down Expand Up @@ -231,10 +231,14 @@ Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync
static Aspire.Hosting.ApplicationModel.ResourceExtensions.HasAnnotationIncludingAncestorsOfType<T>(this Aspire.Hosting.ApplicationModel.IResource! resource) -> bool
static Aspire.Hosting.ApplicationModel.ResourceExtensions.HasAnnotationOfType<T>(this Aspire.Hosting.ApplicationModel.IResource! resource) -> bool
static Aspire.Hosting.ApplicationModel.ResourceExtensions.TryGetAnnotationsIncludingAncestorsOfType<T>(this Aspire.Hosting.ApplicationModel.IResource! resource, out System.Collections.Generic.IEnumerable<T>? result) -> bool
static Aspire.Hosting.ContainerResourceBuilderExtensions.WithBuildArg<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, string! name, object? value) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ContainerResourceBuilderExtensions.WithContainerName<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, string! name) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ContainerResourceBuilderExtensions.WithImageRegistry<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, string? registry) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ContainerResourceBuilderExtensions.WithLifetime<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, Aspire.Hosting.ApplicationModel.ContainerLifetime lifetime) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ContainerResourceBuilderExtensions.WithVolume<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, string? name, string! target, bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ExecutableResourceBuilderExtensions.PublishAsDockerFile<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ExecutableResourceBuilderExtensions.PublishAsDockerFile<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, System.Action<Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ContainerResource!>!>? configure) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ExecutableResourceBuilderExtensions.PublishAsDockerFile<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, System.Collections.Generic.IEnumerable<Aspire.Hosting.ApplicationModel.DockerBuildArg!>? buildArgs) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ParameterResourceBuilderExtensions.AddParameter(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, Aspire.Hosting.ApplicationModel.ParameterDefault! value, bool secret = false, bool persist = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>!
static Aspire.Hosting.ParameterResourceBuilderExtensions.AddParameter(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string! value, bool publishValueAsDefault = false, bool secret = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>!
static Aspire.Hosting.ParameterResourceBuilderExtensions.AddParameter(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, System.Func<string!>! valueGetter, bool publishValueAsDefault = false, bool secret = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>!
Expand Down Expand Up @@ -274,7 +278,6 @@ Aspire.Hosting.IDistributedApplicationBuilder.AppHostAssembly.get -> System.Refl
static Aspire.Hosting.ContainerResourceBuilderExtensions.AddDockerfile(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string! contextPath, string? dockerfilePath = null, string? stage = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ContainerResource!>!
static Aspire.Hosting.ContainerResourceBuilderExtensions.WithDockerfile<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, string! contextPath, string? dockerfilePath = null, string? stage = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ContainerResourceBuilderExtensions.WithBuildArg<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, string! name, Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>! value) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ContainerResourceBuilderExtensions.WithBuildArg<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, string! name, object! value) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ContainerResourceBuilderExtensions.WithBuildSecret<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, string! name, Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>! value) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ProjectResourceBuilderExtensions.AddProject(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string! projectPath, System.Action<Aspire.Hosting.ProjectResourceOptions!>! configure) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ProjectResource!>!
static Aspire.Hosting.ProjectResourceBuilderExtensions.AddProject<TProject>(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, System.Action<Aspire.Hosting.ProjectResourceOptions!>! configure) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ProjectResource!>!
Expand Down
33 changes: 25 additions & 8 deletions tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ public async Task AddPythonAppProducesDockerfileResourceInManifest()
var manifest = await ManifestUtils.GetManifest(pyproj.Resource, manifestDirectory: projectDirectory);
var expectedManifest = $$"""
{
"type": "dockerfile.v0",
"path": "Dockerfile",
"context": "."
"type": "container.v1",
"build": {
"context": ".",
"dockerfile": "Dockerfile"
}
}
""";
Assert.Equal(expectedManifest, manifest.ToString());
Assert.Equal(expectedManifest, manifest.ToString(), ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true);

// If we don't throw, clean up the directories.
Directory.Delete(projectDirectory, true);
Expand All @@ -64,15 +66,18 @@ public async Task AddInstrumentedPythonProjectProducesDockerfileResourceInManife
var manifest = await ManifestUtils.GetManifest(pyproj.Resource, manifestDirectory: projectDirectory);
var expectedManifest = $$"""
{
"type": "dockerfile.v0",
"path": "Dockerfile",
"context": ".",
"type": "container.v1",
"build": {
"context": ".",
"dockerfile": "Dockerfile"
},
"env": {
"OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED": "true"
}
}
""";
Assert.Equal(expectedManifest, manifest.ToString());

Assert.Equal(expectedManifest, manifest.ToString(), ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true);

// If we don't throw, clean up the directories.
Directory.Delete(projectDirectory, true);
Expand Down Expand Up @@ -269,6 +274,18 @@ private static void PreparePythonProject(ITestOutputHelper outputHelper, string
var requirementsPath = Path.Combine(projectDirectory, "requirements.txt");
File.WriteAllText(requirementsPath, requirementsContent);

// This dockerfile doesn't *need* to work but it's a good sanity check.
var dockerFilePath = Path.Combine(projectDirectory, "Dockerfile");
File.WriteAllText(dockerFilePath,
"""
FROM python:3.9
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]
""");

var prepareVirtualEnvironmentStartInfo = new ProcessStartInfo()
{
FileName = "python",
Expand Down
Loading
Loading