From 0d2935b7129027ee8e2d25fb1560b14a60b78c7b Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Fri, 3 Apr 2026 15:42:16 -0700 Subject: [PATCH 1/3] Expose Dockerfile builder APIs to polyglot apphosts Re-enable the Dockerfile builder callback surface for TypeScript, Python, and Java apphosts through ATS-safe wrapper exports. Also add curated container-files wrapper exports, fix generator handling for duplicate handle/builder type IDs and run-capability naming, update the real polyglot validation apphosts, and refresh the affected codegen snapshots. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AtsJavaCodeGenerator.cs | 7 +- .../AtsPythonCodeGenerator.cs | 24 + .../Docker/ContainerFilesExtensions.cs | 2 + .../Docker/DockerfileBuilder.cs | 1 + .../Docker/DockerfileStage.cs | 1 + .../DockerfileBuilderCallbackContext.cs | 3 +- .../Ats/DockerfileBuilderExports.cs | 145 ++++ .../ContainerResourceBuilderExtensions.cs | 10 +- ...oPassScanningGeneratedAspire.verified.java | 424 ++++++++++ ...TwoPassScanningGeneratedAspire.verified.py | 327 +++++++- ...ContainerResourceCapabilities.verified.txt | 14 + ...TwoPassScanningGeneratedAspire.verified.ts | 740 +++++++++++++++++- .../Aspire.Hosting/Java/AppHost.java | 28 + .../Aspire.Hosting/Python/apphost.py | 30 + .../Aspire.Hosting/TypeScript/apphost.ts | 34 + 15 files changed, 1780 insertions(+), 10 deletions(-) create mode 100644 src/Aspire.Hosting/Ats/DockerfileBuilderExports.cs diff --git a/src/Aspire.Hosting.CodeGeneration.Java/AtsJavaCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.Java/AtsJavaCodeGenerator.cs index 2749f453c04..482ac86fc09 100644 --- a/src/Aspire.Hosting.CodeGeneration.Java/AtsJavaCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.Java/AtsJavaCodeGenerator.cs @@ -1663,7 +1663,12 @@ private IReadOnlyList BuildHandleTypes(AtsContext context) _classNames[typeId] = CreateClassName(typeId); } - var handleTypeMap = context.HandleTypes.ToDictionary(t => t.AtsTypeId, StringComparer.Ordinal); + var handleTypeMap = context.HandleTypes + .GroupBy(t => t.AtsTypeId, StringComparer.Ordinal) + .ToDictionary( + g => g.Key, + g => g.OrderByDescending(t => t.IsResourceBuilder).First(), + StringComparer.Ordinal); var results = new List(); foreach (var typeId in handleTypeIds) { diff --git a/src/Aspire.Hosting.CodeGeneration.Python/AtsPythonCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.Python/AtsPythonCodeGenerator.cs index ef5b31fdd51..e5ec8b4941c 100644 --- a/src/Aspire.Hosting.CodeGeneration.Python/AtsPythonCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.Python/AtsPythonCodeGenerator.cs @@ -1751,6 +1751,30 @@ private List CreateBuilderModels(IReadOnlyList /// private static List TopologicalSortBuilders(List builders) { + builders = builders + .GroupBy(b => b.TypeId, StringComparer.Ordinal) + .Select(g => + { + if (g.Count() == 1) + { + return g.First(); + } + + var first = g.First(); + return new BuilderModel + { + TypeId = g.Key, + BuilderClassName = first.BuilderClassName, + Capabilities = g.SelectMany(b => b.Capabilities) + .GroupBy(c => c.CapabilityId, StringComparer.Ordinal) + .Select(cg => cg.First()) + .ToList(), + IsInterface = first.IsInterface, + TargetType = g.Select(b => b.TargetType).FirstOrDefault(t => t is not null) + }; + }) + .ToList(); + var buildersByTypeId = builders.ToDictionary(b => b.TypeId, StringComparer.Ordinal); var result = new List(); var visited = new HashSet(StringComparer.Ordinal); diff --git a/src/Aspire.Hosting/ApplicationModel/Docker/ContainerFilesExtensions.cs b/src/Aspire.Hosting/ApplicationModel/Docker/ContainerFilesExtensions.cs index 38cccf7aaf3..551898a3f5b 100644 --- a/src/Aspire.Hosting/ApplicationModel/Docker/ContainerFilesExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/Docker/ContainerFilesExtensions.cs @@ -19,6 +19,7 @@ public static class ContainerFilesExtensions /// The resource containing container files to be added to the Dockerfile. Cannot be null. /// An optional logger used to record warnings if container image names cannot be determined for source resources. /// The same DockerfileBuilder instance with additional instructions for container files, enabling method chaining. + [AspireExportIgnore(Reason = "Polyglot-facing wrappers are exported from DockerfileBuilderExports with a curated signature.")] [Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static DockerfileBuilder AddContainerFilesStages(this DockerfileBuilder builder, IResource resource, ILogger? logger) { @@ -76,6 +77,7 @@ public static DockerfileBuilder AddContainerFilesStages(this DockerfileBuilder b /// such as copying static assets from a frontend build container into a backend API container. /// /// + [AspireExportIgnore(Reason = "Polyglot-facing wrappers are exported from DockerfileBuilderExports with a curated signature.")] [Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static DockerfileStage AddContainerFiles(this DockerfileStage stage, IResource resource, string rootDestinationPath, ILogger? logger) { diff --git a/src/Aspire.Hosting/ApplicationModel/Docker/DockerfileBuilder.cs b/src/Aspire.Hosting/ApplicationModel/Docker/DockerfileBuilder.cs index 89f2dd0c672..a62559dc6c4 100644 --- a/src/Aspire.Hosting/ApplicationModel/Docker/DockerfileBuilder.cs +++ b/src/Aspire.Hosting/ApplicationModel/Docker/DockerfileBuilder.cs @@ -8,6 +8,7 @@ namespace Aspire.Hosting.ApplicationModel.Docker; /// /// Builder for creating Dockerfiles programmatically. /// +[AspireExport] [Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public class DockerfileBuilder { diff --git a/src/Aspire.Hosting/ApplicationModel/Docker/DockerfileStage.cs b/src/Aspire.Hosting/ApplicationModel/Docker/DockerfileStage.cs index 315610ed0b8..edbf71ca8be 100644 --- a/src/Aspire.Hosting/ApplicationModel/Docker/DockerfileStage.cs +++ b/src/Aspire.Hosting/ApplicationModel/Docker/DockerfileStage.cs @@ -8,6 +8,7 @@ namespace Aspire.Hosting.ApplicationModel.Docker; /// /// Represents a stage within a multi-stage Dockerfile. /// +[AspireExport] [Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public class DockerfileStage : DockerfileStatement { diff --git a/src/Aspire.Hosting/ApplicationModel/DockerfileBuilderCallbackContext.cs b/src/Aspire.Hosting/ApplicationModel/DockerfileBuilderCallbackContext.cs index 28a1d5abdcf..df5568ca800 100644 --- a/src/Aspire.Hosting/ApplicationModel/DockerfileBuilderCallbackContext.cs +++ b/src/Aspire.Hosting/ApplicationModel/DockerfileBuilderCallbackContext.cs @@ -9,6 +9,7 @@ namespace Aspire.Hosting.ApplicationModel; /// /// Provides context information for Dockerfile build callbacks. /// +[AspireExport(ExposeProperties = true)] [Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public class DockerfileBuilderCallbackContext { @@ -46,4 +47,4 @@ public DockerfileBuilderCallbackContext(IResource resource, DockerfileBuilder bu /// Gets the cancellation token to observe while waiting for the task to complete. /// public CancellationToken CancellationToken { get; } -} \ No newline at end of file +} diff --git a/src/Aspire.Hosting/Ats/DockerfileBuilderExports.cs b/src/Aspire.Hosting/Ats/DockerfileBuilderExports.cs new file mode 100644 index 00000000000..119a3820b23 --- /dev/null +++ b/src/Aspire.Hosting/Ats/DockerfileBuilderExports.cs @@ -0,0 +1,145 @@ +// 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 ASPIREDOCKERFILEBUILDER001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.ApplicationModel.Docker; +using Microsoft.Extensions.Logging; + +namespace Aspire.Hosting.Ats; + +/// +/// ATS exports for the Dockerfile builder DSL. +/// +internal static class DockerfileBuilderExports +{ + [AspireExport("dockerfileBuilderArg", MethodName = "arg", Description = "Adds a global ARG statement to the Dockerfile")] + public static DockerfileBuilder Arg(this DockerfileBuilder builder, string name, string? defaultValue = null) + { + ArgumentNullException.ThrowIfNull(builder); + + return defaultValue is null ? builder.Arg(name) : builder.Arg(name, defaultValue); + } + + [AspireExport("dockerfileBuilderFrom", MethodName = "from", Description = "Adds a FROM statement to start a Dockerfile stage")] + public static DockerfileStage From(this DockerfileBuilder builder, string image, string? stageName = null) + { + ArgumentNullException.ThrowIfNull(builder); + + return stageName is null ? builder.From(image) : builder.From(image, stageName); + } + + [AspireExport("dockerfileStageArg", MethodName = "arg", Description = "Adds an ARG statement to a Dockerfile stage")] + public static DockerfileStage Arg(this DockerfileStage stage, string name, string? defaultValue = null) + { + ArgumentNullException.ThrowIfNull(stage); + + return defaultValue is null ? stage.Arg(name) : stage.Arg(name, defaultValue); + } + + [AspireExport(Description = "Adds a WORKDIR statement to a Dockerfile stage")] + public static DockerfileStage WorkDir(this DockerfileStage stage, string path) + { + ArgumentNullException.ThrowIfNull(stage); + return stage.WorkDir(path); + } + + [AspireExport("dockerfileStageRun", MethodName = "run", Description = "Adds a RUN statement to a Dockerfile stage")] + public static DockerfileStage Run(this DockerfileStage stage, string command) + { + ArgumentNullException.ThrowIfNull(stage); + return stage.Run(command); + } + + [AspireExport("dockerfileStageCopy", MethodName = "copy", Description = "Adds a COPY statement to a Dockerfile stage")] + public static DockerfileStage Copy(this DockerfileStage stage, string source, string destination, string? chown = null) + { + ArgumentNullException.ThrowIfNull(stage); + + return chown is null ? stage.Copy(source, destination) : stage.Copy(source, destination, chown); + } + + [AspireExport("dockerfileStageCopyFrom", MethodName = "copyFrom", Description = "Adds a COPY --from statement to a Dockerfile stage")] + public static DockerfileStage CopyFrom(this DockerfileStage stage, string from, string source, string destination, string? chown = null) + { + ArgumentNullException.ThrowIfNull(stage); + + return chown is null ? stage.CopyFrom(from, source, destination) : stage.CopyFrom(from, source, destination, chown); + } + + [AspireExport(Description = "Adds an ENV statement to a Dockerfile stage")] + public static DockerfileStage Env(this DockerfileStage stage, string name, string value) + { + ArgumentNullException.ThrowIfNull(stage); + return stage.Env(name, value); + } + + [AspireExport(Description = "Adds an EXPOSE statement to a Dockerfile stage")] + public static DockerfileStage Expose(this DockerfileStage stage, int port) + { + ArgumentNullException.ThrowIfNull(stage); + return stage.Expose(port); + } + + [AspireExport(Description = "Adds a CMD statement to a Dockerfile stage")] + public static DockerfileStage Cmd(this DockerfileStage stage, string[] command) + { + ArgumentNullException.ThrowIfNull(stage); + return stage.Cmd(command); + } + + [AspireExport(Description = "Adds an ENTRYPOINT statement to a Dockerfile stage")] + public static DockerfileStage Entrypoint(this DockerfileStage stage, string[] command) + { + ArgumentNullException.ThrowIfNull(stage); + return stage.Entrypoint(command); + } + + [AspireExport(Description = "Adds a RUN statement with mounts to a Dockerfile stage")] + public static DockerfileStage RunWithMounts(this DockerfileStage stage, string command, string[] mounts) + { + ArgumentNullException.ThrowIfNull(stage); + return stage.RunWithMounts(command, mounts); + } + + [AspireExport(Description = "Adds a USER statement to a Dockerfile stage")] + public static DockerfileStage User(this DockerfileStage stage, string user) + { + ArgumentNullException.ThrowIfNull(stage); + return stage.User(user); + } + + [AspireExport(Description = "Adds an empty line to a Dockerfile stage")] + public static DockerfileStage EmptyLine(this DockerfileStage stage) + { + ArgumentNullException.ThrowIfNull(stage); + return stage.EmptyLine(); + } + + [AspireExport(Description = "Adds a comment to a Dockerfile stage")] + public static DockerfileStage Comment(this DockerfileStage stage, string comment) + { + ArgumentNullException.ThrowIfNull(stage); + return stage.Comment(comment); + } + + [AspireExport("dockerfileBuilderAddContainerFilesStages", MethodName = "addContainerFilesStages", Description = "Adds Dockerfile stages for published container files")] + public static DockerfileBuilder AddContainerFilesStages(this DockerfileBuilder builder, IResource resource, ILogger? logger = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(resource); + + return ContainerFilesExtensions.AddContainerFilesStages(builder, resource, logger); + } + + [AspireExport("dockerfileStageAddContainerFiles", MethodName = "addContainerFiles", Description = "Adds COPY --from statements for published container files")] + public static DockerfileStage AddContainerFiles(this DockerfileStage stage, IResource resource, string rootDestinationPath, ILogger? logger = null) + { + ArgumentNullException.ThrowIfNull(stage); + ArgumentNullException.ThrowIfNull(resource); + ArgumentException.ThrowIfNullOrEmpty(rootDestinationPath); + + return ContainerFilesExtensions.AddContainerFiles(stage, resource, rootDestinationPath, logger); + } +} diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index fa808b2e85d..74d61936686 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -924,8 +924,7 @@ public static IResourceBuilder AddDockerfileFactory(this IDis /// /// /// - /// This method is not available in polyglot app hosts. Use instead. - [AspireExportIgnore(Reason = "DockerfileBuilderCallbackContext is not an ATS-exported callback context.")] + [AspireExport(Description = "Adds a container resource built from a programmatically generated Dockerfile")] [Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static IResourceBuilder AddDockerfileBuilder(this IDistributedApplicationBuilder builder, [ResourceName] string name, string contextPath, Func callback, string? stage = null) { @@ -972,7 +971,7 @@ public static IResourceBuilder AddDockerfileBuilder(this IDis /// /// /// - /// This method is not available in polyglot app hosts. Use instead. + /// This synchronous overload is not available in polyglot app hosts. Use the overload that accepts a . [AspireExportIgnore(Reason = "DockerfileBuilderCallbackContext is not an ATS-exported callback context.")] [Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static IResourceBuilder AddDockerfileBuilder(this IDistributedApplicationBuilder builder, [ResourceName] string name, string contextPath, Action callback, string? stage = null) @@ -1430,8 +1429,7 @@ public static IResourceBuilder WithEndpointProxySupport(this IResourceBuil /// /// /// - /// This method is not available in polyglot app hosts. Use instead. - [AspireExportIgnore(Reason = "DockerfileBuilderCallbackContext is not an ATS-exported callback context.")] + [AspireExport(Description = "Configures the resource to use a programmatically generated Dockerfile")] [Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static IResourceBuilder WithDockerfileBuilder(this IResourceBuilder builder, string contextPath, Func callback, string? stage = null) where T : ContainerResource { @@ -1535,7 +1533,7 @@ public static IResourceBuilder WithDockerfileBuilder(this IResourceBuilder /// /// /// - /// This method is not available in polyglot app hosts. Use instead. + /// This synchronous overload is not available in polyglot app hosts. Use the overload that accepts a . [AspireExportIgnore(Reason = "DockerfileBuilderCallbackContext is not an ATS-exported callback context.")] [Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static IResourceBuilder WithDockerfileBuilder(this IResourceBuilder builder, string contextPath, Action callback, string? stage = null) where T : ContainerResource diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index 9c19bdd9406..e7e83cbbf21 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -1077,6 +1077,7 @@ public class AspireRegistrations { AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.CommandLineArgsCallbackContext", (h, c) -> new CommandLineArgsCallbackContext(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.ConnectionStringAvailableEvent", (h, c) -> new ConnectionStringAvailableEvent(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.DistributedApplicationModel", (h, c) -> new DistributedApplicationModel(h, c)); + AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.DockerfileBuilderCallbackContext", (h, c) -> new DockerfileBuilderCallbackContext(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.EndpointReferenceExpression", (h, c) -> new EndpointReferenceExpression(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.EnvironmentCallbackContext", (h, c) -> new EnvironmentCallbackContext(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.IExpressionValue", (h, c) -> new IExpressionValue(h, c)); @@ -1088,6 +1089,8 @@ public class AspireRegistrations { AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.ResourceReadyEvent", (h, c) -> new ResourceReadyEvent(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.ResourceStoppedEvent", (h, c) -> new ResourceStoppedEvent(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.ResourceUrlsCallbackContext", (h, c) -> new ResourceUrlsCallbackContext(h, c)); + AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.Docker.DockerfileBuilder", (h, c) -> new DockerfileBuilder(h, c)); + AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.Docker.DockerfileStage", (h, c) -> new DockerfileStage(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ConnectionStringResource", (h, c) -> new ConnectionStringResource(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerRegistryResource", (h, c) -> new ContainerRegistryResource(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.DotnetToolResource", (h, c) -> new DotnetToolResource(h, c)); @@ -4706,6 +4709,30 @@ public ContainerResource withEndpointProxySupport(boolean proxyEnabled) { return this; } + public ContainerResource withDockerfileBuilder(String contextPath, AspireAction1 callback) { + return withDockerfileBuilder(contextPath, callback, null); + } + + /** Configures the resource to use a programmatically generated Dockerfile */ + public ContainerResource withDockerfileBuilder(String contextPath, AspireAction1 callback, String stage) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("contextPath", AspireClient.serializeValue(contextPath)); + var callbackId = getClient().registerCallback(args -> { + var arg = (DockerfileBuilderCallbackContext) args[0]; + callback.invoke(arg); + return null; + }); + if (callbackId != null) { + reqArgs.put("callback", callbackId); + } + if (stage != null) { + reqArgs.put("stage", AspireClient.serializeValue(stage)); + } + getClient().invokeCapability("Aspire.Hosting/withDockerfileBuilder", reqArgs); + return this; + } + /** Sets the base image for a Dockerfile build */ public ContainerResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { var buildImage = options == null ? null : options.getBuildImage(); @@ -6306,6 +6333,304 @@ public class DistributedApplicationResourceEventSubscription extends HandleWrapp } +// ===== DockerfileBuilder.java ===== +// DockerfileBuilder.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** Wrapper for Aspire.Hosting/Aspire.Hosting.ApplicationModel.Docker.DockerfileBuilder. */ +public class DockerfileBuilder extends HandleWrapperBase { + DockerfileBuilder(Handle handle, AspireClient client) { + super(handle, client); + } + + public DockerfileBuilder arg(String name) { + return arg(name, null); + } + + /** Adds a global ARG statement to the Dockerfile */ + public DockerfileBuilder arg(String name, String defaultValue) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + if (defaultValue != null) { + reqArgs.put("defaultValue", AspireClient.serializeValue(defaultValue)); + } + return (DockerfileBuilder) getClient().invokeCapability("Aspire.Hosting/dockerfileBuilderArg", reqArgs); + } + + public DockerfileStage from(String image) { + return from(image, null); + } + + /** Adds a FROM statement to start a Dockerfile stage */ + public DockerfileStage from(String image, String stageName) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("image", AspireClient.serializeValue(image)); + if (stageName != null) { + reqArgs.put("stageName", AspireClient.serializeValue(stageName)); + } + return (DockerfileStage) getClient().invokeCapability("Aspire.Hosting/dockerfileBuilderFrom", reqArgs); + } + + public DockerfileBuilder addContainerFilesStages(IResource resource) { + return addContainerFilesStages(resource, null); + } + + public DockerfileBuilder addContainerFilesStages(ResourceBuilderBase resource) { + return addContainerFilesStages(new IResource(resource.getHandle(), resource.getClient())); + } + + /** Adds Dockerfile stages for published container files */ + public DockerfileBuilder addContainerFilesStages(IResource resource, ILogger logger) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("resource", AspireClient.serializeValue(resource)); + if (logger != null) { + reqArgs.put("logger", AspireClient.serializeValue(logger)); + } + return (DockerfileBuilder) getClient().invokeCapability("Aspire.Hosting/dockerfileBuilderAddContainerFilesStages", reqArgs); + } + + public DockerfileBuilder addContainerFilesStages(ResourceBuilderBase resource, ILogger logger) { + return addContainerFilesStages(new IResource(resource.getHandle(), resource.getClient()), logger); + } + + public DockerfileBuilder addContainerFilesStages(IResource resource, HandleWrapperBase logger) { + return addContainerFilesStages(resource, new ILogger(logger.getHandle(), logger.getClient())); + } + + public DockerfileBuilder addContainerFilesStages(ResourceBuilderBase resource, HandleWrapperBase logger) { + return addContainerFilesStages(new IResource(resource.getHandle(), resource.getClient()), new ILogger(logger.getHandle(), logger.getClient())); + } + +} + +// ===== DockerfileBuilderCallbackContext.java ===== +// DockerfileBuilderCallbackContext.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** Wrapper for Aspire.Hosting/Aspire.Hosting.ApplicationModel.DockerfileBuilderCallbackContext. */ +public class DockerfileBuilderCallbackContext extends HandleWrapperBase { + DockerfileBuilderCallbackContext(Handle handle, AspireClient client) { + super(handle, client); + } + + /** Gets the Resource property */ + public IResource resource() { + Map reqArgs = new HashMap<>(); + reqArgs.put("context", AspireClient.serializeValue(getHandle())); + return (IResource) getClient().invokeCapability("Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.resource", reqArgs); + } + + /** Gets the Builder property */ + public DockerfileBuilder builder() { + Map reqArgs = new HashMap<>(); + reqArgs.put("context", AspireClient.serializeValue(getHandle())); + return (DockerfileBuilder) getClient().invokeCapability("Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.builder", reqArgs); + } + + /** Gets the Services property */ + public IServiceProvider services() { + Map reqArgs = new HashMap<>(); + reqArgs.put("context", AspireClient.serializeValue(getHandle())); + return (IServiceProvider) getClient().invokeCapability("Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.services", reqArgs); + } + + /** Gets the CancellationToken property */ + public CancellationToken cancellationToken() { + Map reqArgs = new HashMap<>(); + reqArgs.put("context", AspireClient.serializeValue(getHandle())); + return (CancellationToken) getClient().invokeCapability("Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.cancellationToken", reqArgs); + } + +} + +// ===== DockerfileStage.java ===== +// DockerfileStage.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** Wrapper for Aspire.Hosting/Aspire.Hosting.ApplicationModel.Docker.DockerfileStage. */ +public class DockerfileStage extends HandleWrapperBase { + DockerfileStage(Handle handle, AspireClient client) { + super(handle, client); + } + + public DockerfileStage arg(String name) { + return arg(name, null); + } + + /** Adds an ARG statement to a Dockerfile stage */ + public DockerfileStage arg(String name, String defaultValue) { + Map reqArgs = new HashMap<>(); + reqArgs.put("stage", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + if (defaultValue != null) { + reqArgs.put("defaultValue", AspireClient.serializeValue(defaultValue)); + } + return (DockerfileStage) getClient().invokeCapability("Aspire.Hosting/dockerfileStageArg", reqArgs); + } + + /** Adds a WORKDIR statement to a Dockerfile stage */ + public DockerfileStage workDir(String path) { + Map reqArgs = new HashMap<>(); + reqArgs.put("stage", AspireClient.serializeValue(getHandle())); + reqArgs.put("path", AspireClient.serializeValue(path)); + return (DockerfileStage) getClient().invokeCapability("Aspire.Hosting/workDir", reqArgs); + } + + /** Adds a RUN statement to a Dockerfile stage */ + public DockerfileStage run(String command) { + Map reqArgs = new HashMap<>(); + reqArgs.put("stage", AspireClient.serializeValue(getHandle())); + reqArgs.put("command", AspireClient.serializeValue(command)); + return (DockerfileStage) getClient().invokeCapability("Aspire.Hosting/dockerfileStageRun", reqArgs); + } + + public DockerfileStage copy(String source, String destination) { + return copy(source, destination, null); + } + + /** Adds a COPY statement to a Dockerfile stage */ + public DockerfileStage copy(String source, String destination, String chown) { + Map reqArgs = new HashMap<>(); + reqArgs.put("stage", AspireClient.serializeValue(getHandle())); + reqArgs.put("source", AspireClient.serializeValue(source)); + reqArgs.put("destination", AspireClient.serializeValue(destination)); + if (chown != null) { + reqArgs.put("chown", AspireClient.serializeValue(chown)); + } + return (DockerfileStage) getClient().invokeCapability("Aspire.Hosting/dockerfileStageCopy", reqArgs); + } + + public DockerfileStage copyFrom(String from, String source, String destination) { + return copyFrom(from, source, destination, null); + } + + /** Adds a COPY --from statement to a Dockerfile stage */ + public DockerfileStage copyFrom(String from, String source, String destination, String chown) { + Map reqArgs = new HashMap<>(); + reqArgs.put("stage", AspireClient.serializeValue(getHandle())); + reqArgs.put("from", AspireClient.serializeValue(from)); + reqArgs.put("source", AspireClient.serializeValue(source)); + reqArgs.put("destination", AspireClient.serializeValue(destination)); + if (chown != null) { + reqArgs.put("chown", AspireClient.serializeValue(chown)); + } + return (DockerfileStage) getClient().invokeCapability("Aspire.Hosting/dockerfileStageCopyFrom", reqArgs); + } + + /** Adds an ENV statement to a Dockerfile stage */ + public DockerfileStage env(String name, String value) { + Map reqArgs = new HashMap<>(); + reqArgs.put("stage", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + reqArgs.put("value", AspireClient.serializeValue(value)); + return (DockerfileStage) getClient().invokeCapability("Aspire.Hosting/env", reqArgs); + } + + /** Adds an EXPOSE statement to a Dockerfile stage */ + public DockerfileStage expose(double port) { + Map reqArgs = new HashMap<>(); + reqArgs.put("stage", AspireClient.serializeValue(getHandle())); + reqArgs.put("port", AspireClient.serializeValue(port)); + return (DockerfileStage) getClient().invokeCapability("Aspire.Hosting/expose", reqArgs); + } + + /** Adds a CMD statement to a Dockerfile stage */ + public DockerfileStage cmd(String[] command) { + Map reqArgs = new HashMap<>(); + reqArgs.put("stage", AspireClient.serializeValue(getHandle())); + reqArgs.put("command", AspireClient.serializeValue(command)); + return (DockerfileStage) getClient().invokeCapability("Aspire.Hosting/cmd", reqArgs); + } + + /** Adds an ENTRYPOINT statement to a Dockerfile stage */ + public DockerfileStage entrypoint(String[] command) { + Map reqArgs = new HashMap<>(); + reqArgs.put("stage", AspireClient.serializeValue(getHandle())); + reqArgs.put("command", AspireClient.serializeValue(command)); + return (DockerfileStage) getClient().invokeCapability("Aspire.Hosting/entrypoint", reqArgs); + } + + /** Adds a RUN statement with mounts to a Dockerfile stage */ + public DockerfileStage runWithMounts(String command, String[] mounts) { + Map reqArgs = new HashMap<>(); + reqArgs.put("stage", AspireClient.serializeValue(getHandle())); + reqArgs.put("command", AspireClient.serializeValue(command)); + reqArgs.put("mounts", AspireClient.serializeValue(mounts)); + return (DockerfileStage) getClient().invokeCapability("Aspire.Hosting/runWithMounts", reqArgs); + } + + /** Adds a USER statement to a Dockerfile stage */ + public DockerfileStage user(String user) { + Map reqArgs = new HashMap<>(); + reqArgs.put("stage", AspireClient.serializeValue(getHandle())); + reqArgs.put("user", AspireClient.serializeValue(user)); + return (DockerfileStage) getClient().invokeCapability("Aspire.Hosting/user", reqArgs); + } + + /** Adds an empty line to a Dockerfile stage */ + public DockerfileStage emptyLine() { + Map reqArgs = new HashMap<>(); + reqArgs.put("stage", AspireClient.serializeValue(getHandle())); + return (DockerfileStage) getClient().invokeCapability("Aspire.Hosting/emptyLine", reqArgs); + } + + /** Adds a comment to a Dockerfile stage */ + public DockerfileStage comment(String comment) { + Map reqArgs = new HashMap<>(); + reqArgs.put("stage", AspireClient.serializeValue(getHandle())); + reqArgs.put("comment", AspireClient.serializeValue(comment)); + return (DockerfileStage) getClient().invokeCapability("Aspire.Hosting/comment", reqArgs); + } + + public DockerfileStage addContainerFiles(IResource resource, String rootDestinationPath) { + return addContainerFiles(resource, rootDestinationPath, null); + } + + public DockerfileStage addContainerFiles(ResourceBuilderBase resource, String rootDestinationPath) { + return addContainerFiles(new IResource(resource.getHandle(), resource.getClient()), rootDestinationPath); + } + + /** Adds COPY --from statements for published container files */ + public DockerfileStage addContainerFiles(IResource resource, String rootDestinationPath, ILogger logger) { + Map reqArgs = new HashMap<>(); + reqArgs.put("stage", AspireClient.serializeValue(getHandle())); + reqArgs.put("resource", AspireClient.serializeValue(resource)); + reqArgs.put("rootDestinationPath", AspireClient.serializeValue(rootDestinationPath)); + if (logger != null) { + reqArgs.put("logger", AspireClient.serializeValue(logger)); + } + return (DockerfileStage) getClient().invokeCapability("Aspire.Hosting/dockerfileStageAddContainerFiles", reqArgs); + } + + public DockerfileStage addContainerFiles(ResourceBuilderBase resource, String rootDestinationPath, ILogger logger) { + return addContainerFiles(new IResource(resource.getHandle(), resource.getClient()), rootDestinationPath, logger); + } + + public DockerfileStage addContainerFiles(IResource resource, String rootDestinationPath, HandleWrapperBase logger) { + return addContainerFiles(resource, rootDestinationPath, new ILogger(logger.getHandle(), logger.getClient())); + } + + public DockerfileStage addContainerFiles(ResourceBuilderBase resource, String rootDestinationPath, HandleWrapperBase logger) { + return addContainerFiles(new IResource(resource.getHandle(), resource.getClient()), rootDestinationPath, new ILogger(logger.getHandle(), logger.getClient())); + } + +} + // ===== DotnetToolResource.java ===== // DotnetToolResource.java - GENERATED CODE - DO NOT EDIT @@ -10453,6 +10778,30 @@ private ContainerResource addDockerfileImpl(String name, String contextPath, Str return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/addDockerfile", reqArgs); } + public ContainerResource addDockerfileBuilder(String name, String contextPath, AspireAction1 callback) { + return addDockerfileBuilder(name, contextPath, callback, null); + } + + /** Adds a container resource built from a programmatically generated Dockerfile */ + public ContainerResource addDockerfileBuilder(String name, String contextPath, AspireAction1 callback, String stage) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + reqArgs.put("contextPath", AspireClient.serializeValue(contextPath)); + var callbackId = getClient().registerCallback(args -> { + var arg = (DockerfileBuilderCallbackContext) args[0]; + callback.invoke(arg); + return null; + }); + if (callbackId != null) { + reqArgs.put("callback", callbackId); + } + if (stage != null) { + reqArgs.put("stage", AspireClient.serializeValue(stage)); + } + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/addDockerfileBuilder", reqArgs); + } + /** Adds a .NET tool resource */ public DotnetToolResource addDotnetTool(String name, String packageId) { Map reqArgs = new HashMap<>(); @@ -15145,6 +15494,30 @@ public TestDatabaseResource withEndpointProxySupport(boolean proxyEnabled) { return this; } + public TestDatabaseResource withDockerfileBuilder(String contextPath, AspireAction1 callback) { + return withDockerfileBuilder(contextPath, callback, null); + } + + /** Configures the resource to use a programmatically generated Dockerfile */ + public TestDatabaseResource withDockerfileBuilder(String contextPath, AspireAction1 callback, String stage) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("contextPath", AspireClient.serializeValue(contextPath)); + var callbackId = getClient().registerCallback(args -> { + var arg = (DockerfileBuilderCallbackContext) args[0]; + callback.invoke(arg); + return null; + }); + if (callbackId != null) { + reqArgs.put("callback", callbackId); + } + if (stage != null) { + reqArgs.put("stage", AspireClient.serializeValue(stage)); + } + getClient().invokeCapability("Aspire.Hosting/withDockerfileBuilder", reqArgs); + return this; + } + /** Sets the base image for a Dockerfile build */ public TestDatabaseResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { var buildImage = options == null ? null : options.getBuildImage(); @@ -16825,6 +17198,30 @@ public TestRedisResource withEndpointProxySupport(boolean proxyEnabled) { return this; } + public TestRedisResource withDockerfileBuilder(String contextPath, AspireAction1 callback) { + return withDockerfileBuilder(contextPath, callback, null); + } + + /** Configures the resource to use a programmatically generated Dockerfile */ + public TestRedisResource withDockerfileBuilder(String contextPath, AspireAction1 callback, String stage) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("contextPath", AspireClient.serializeValue(contextPath)); + var callbackId = getClient().registerCallback(args -> { + var arg = (DockerfileBuilderCallbackContext) args[0]; + callback.invoke(arg); + return null; + }); + if (callbackId != null) { + reqArgs.put("callback", callbackId); + } + if (stage != null) { + reqArgs.put("stage", AspireClient.serializeValue(stage)); + } + getClient().invokeCapability("Aspire.Hosting/withDockerfileBuilder", reqArgs); + return this; + } + /** Sets the base image for a Dockerfile build */ public TestRedisResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { var buildImage = options == null ? null : options.getBuildImage(); @@ -18650,6 +19047,30 @@ public TestVaultResource withEndpointProxySupport(boolean proxyEnabled) { return this; } + public TestVaultResource withDockerfileBuilder(String contextPath, AspireAction1 callback) { + return withDockerfileBuilder(contextPath, callback, null); + } + + /** Configures the resource to use a programmatically generated Dockerfile */ + public TestVaultResource withDockerfileBuilder(String contextPath, AspireAction1 callback, String stage) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("contextPath", AspireClient.serializeValue(contextPath)); + var callbackId = getClient().registerCallback(args -> { + var arg = (DockerfileBuilderCallbackContext) args[0]; + callback.invoke(arg); + return null; + }); + if (callbackId != null) { + reqArgs.put("callback", callbackId); + } + if (stage != null) { + reqArgs.put("stage", AspireClient.serializeValue(stage)); + } + getClient().invokeCapability("Aspire.Hosting/withDockerfileBuilder", reqArgs); + return this; + } + /** Sets the base image for a Dockerfile build */ public TestVaultResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { var buildImage = options == null ? null : options.getBuildImage(); @@ -20726,6 +21147,9 @@ public WithVolumeOptions isReadOnly(Boolean value) { .modules/DistributedApplicationModel.java .modules/DistributedApplicationOperation.java .modules/DistributedApplicationResourceEventSubscription.java +.modules/DockerfileBuilder.java +.modules/DockerfileBuilderCallbackContext.java +.modules/DockerfileStage.java .modules/DotnetToolResource.java .modules/EndpointProperty.java .modules/EndpointReference.java diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index fcc8f4e2a7a..e9610292957 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -1,4 +1,4 @@ -# ------------------------------------------------------------- +# ------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See LICENSE in project root for information. # @@ -1587,6 +1587,12 @@ class DockerfileParameters(typing.TypedDict, total=False): stage: str +class DockerfileBuilderParameters(typing.TypedDict, total=False): + context_path: typing.Required[str] + callback: typing.Required[typing.Callable[[DockerfileBuilderCallbackContext], None]] + stage: str + + class McpServerParameters(typing.TypedDict, total=False): path: str endpoint_name: str @@ -1958,6 +1964,21 @@ def add_dockerfile(self, name: str, context_path: str, *, dockerfile_path: str | ) return typing.cast(ContainerResource, result) + def add_dockerfile_builder(self, name: str, context_path: str, callback: typing.Callable[[DockerfileBuilderCallbackContext], None], *, stage: str | None = None, **kwargs: typing.Unpack["ContainerResourceKwargs"]) -> ContainerResource: # type: ignore + """Adds a container resource built from a programmatically generated Dockerfile""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['name'] = name + rpc_args['contextPath'] = context_path + rpc_args['callback'] = self._client.register_callback(callback) + if stage is not None: + rpc_args['stage'] = stage + result = self._client.invoke_capability( + 'Aspire.Hosting/addDockerfileBuilder', + rpc_args, + kwargs, + ) + return typing.cast(ContainerResource, result) + def add_dotnet_tool(self, name: str, package_id: str, **kwargs: typing.Unpack["DotnetToolResourceKwargs"]) -> DotnetToolResource: # type: ignore """Adds a .NET tool resource""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -3010,6 +3031,278 @@ def find_resource_by_name(self, name: str) -> AbstractResource: return typing.cast(AbstractResource, result) +class DockerfileBuilder: + """Type class for DockerfileBuilder.""" + + def __init__(self, handle: Handle, client: AspireClient) -> None: + self._handle = handle + self._client = client + + def __repr__(self) -> str: + return f"DockerfileBuilder(handle={self._handle.handle_id})" + + @_uncached_property + def handle(self) -> Handle: + """The underlying object reference handle.""" + return self._handle + + def arg(self, name: str, *, default_value: str | None = None) -> DockerfileBuilder: + """Adds a global ARG statement to the Dockerfile""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['name'] = name + if default_value is not None: + rpc_args['defaultValue'] = default_value + result = self._client.invoke_capability( + 'Aspire.Hosting/dockerfileBuilderArg', + rpc_args, + ) + return typing.cast(DockerfileBuilder, result) + + def from_(self, image: str, *, stage_name: str | None = None) -> DockerfileStage: + """Adds a FROM statement to start a Dockerfile stage""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['image'] = image + if stage_name is not None: + rpc_args['stageName'] = stage_name + result = self._client.invoke_capability( + 'Aspire.Hosting/dockerfileBuilderFrom', + rpc_args, + ) + return typing.cast(DockerfileStage, result) + + def add_container_files_stages(self, resource: AbstractResource, *, logger: AbstractLogger | None = None) -> DockerfileBuilder: + """Adds Dockerfile stages for published container files""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['resource'] = resource + if logger is not None: + rpc_args['logger'] = logger + result = self._client.invoke_capability( + 'Aspire.Hosting/dockerfileBuilderAddContainerFilesStages', + rpc_args, + ) + return typing.cast(DockerfileBuilder, result) + + +class DockerfileBuilderCallbackContext: + """Type class for DockerfileBuilderCallbackContext.""" + + def __init__(self, handle: Handle, client: AspireClient) -> None: + self._handle = handle + self._client = client + + def __repr__(self) -> str: + return f"DockerfileBuilderCallbackContext(handle={self._handle.handle_id})" + + @_uncached_property + def handle(self) -> Handle: + """The underlying object reference handle.""" + return self._handle + + @_cached_property + def resource(self) -> AbstractResource: + """Gets the Resource property""" + result = self._client.invoke_capability( + 'Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.resource', + {'context': self._handle} + ) + return typing.cast(AbstractResource, result) + + @_cached_property + def builder(self) -> DockerfileBuilder: + """Gets the Builder property""" + result = self._client.invoke_capability( + 'Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.builder', + {'context': self._handle} + ) + return typing.cast(DockerfileBuilder, result) + + @_cached_property + def services(self) -> AbstractServiceProvider: + """Gets the Services property""" + result = self._client.invoke_capability( + 'Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.services', + {'context': self._handle} + ) + return typing.cast(AbstractServiceProvider, result) + + def cancel(self) -> None: + """Cancel the operation.""" + token: CancellationToken = self._client.invoke_capability( + 'Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.cancellationToken', + {'context': self._handle} + ) + token.cancel() + + +class DockerfileStage: + """Type class for DockerfileStage.""" + + def __init__(self, handle: Handle, client: AspireClient) -> None: + self._handle = handle + self._client = client + + def __repr__(self) -> str: + return f"DockerfileStage(handle={self._handle.handle_id})" + + @_uncached_property + def handle(self) -> Handle: + """The underlying object reference handle.""" + return self._handle + + def arg(self, name: str, *, default_value: str | None = None) -> DockerfileStage: + """Adds an ARG statement to a Dockerfile stage""" + rpc_args: dict[str, typing.Any] = {'stage': self._handle} + rpc_args['name'] = name + if default_value is not None: + rpc_args['defaultValue'] = default_value + result = self._client.invoke_capability( + 'Aspire.Hosting/dockerfileStageArg', + rpc_args, + ) + return typing.cast(DockerfileStage, result) + + def work_dir(self, path: str) -> DockerfileStage: + """Adds a WORKDIR statement to a Dockerfile stage""" + rpc_args: dict[str, typing.Any] = {'stage': self._handle} + rpc_args['path'] = path + result = self._client.invoke_capability( + 'Aspire.Hosting/workDir', + rpc_args, + ) + return typing.cast(DockerfileStage, result) + + def run(self, command: str) -> DockerfileStage: + """Adds a RUN statement to a Dockerfile stage""" + rpc_args: dict[str, typing.Any] = {'stage': self._handle} + rpc_args['command'] = command + result = self._client.invoke_capability( + 'Aspire.Hosting/dockerfileStageRun', + rpc_args, + ) + return typing.cast(DockerfileStage, result) + + def copy(self, source: str, destination: str, *, chown: str | None = None) -> DockerfileStage: + """Adds a COPY statement to a Dockerfile stage""" + rpc_args: dict[str, typing.Any] = {'stage': self._handle} + rpc_args['source'] = source + rpc_args['destination'] = destination + if chown is not None: + rpc_args['chown'] = chown + result = self._client.invoke_capability( + 'Aspire.Hosting/dockerfileStageCopy', + rpc_args, + ) + return typing.cast(DockerfileStage, result) + + def copy_from(self, from_: str, source: str, destination: str, *, chown: str | None = None) -> DockerfileStage: + """Adds a COPY --from statement to a Dockerfile stage""" + rpc_args: dict[str, typing.Any] = {'stage': self._handle} + rpc_args['from'] = from_ + rpc_args['source'] = source + rpc_args['destination'] = destination + if chown is not None: + rpc_args['chown'] = chown + result = self._client.invoke_capability( + 'Aspire.Hosting/dockerfileStageCopyFrom', + rpc_args, + ) + return typing.cast(DockerfileStage, result) + + def env(self, name: str, value: str) -> DockerfileStage: + """Adds an ENV statement to a Dockerfile stage""" + rpc_args: dict[str, typing.Any] = {'stage': self._handle} + rpc_args['name'] = name + rpc_args['value'] = value + result = self._client.invoke_capability( + 'Aspire.Hosting/env', + rpc_args, + ) + return typing.cast(DockerfileStage, result) + + def expose(self, port: int) -> DockerfileStage: + """Adds an EXPOSE statement to a Dockerfile stage""" + rpc_args: dict[str, typing.Any] = {'stage': self._handle} + rpc_args['port'] = port + result = self._client.invoke_capability( + 'Aspire.Hosting/expose', + rpc_args, + ) + return typing.cast(DockerfileStage, result) + + def cmd(self, command: typing.Iterable[str]) -> DockerfileStage: + """Adds a CMD statement to a Dockerfile stage""" + rpc_args: dict[str, typing.Any] = {'stage': self._handle} + rpc_args['command'] = command + result = self._client.invoke_capability( + 'Aspire.Hosting/cmd', + rpc_args, + ) + return typing.cast(DockerfileStage, result) + + def entrypoint(self, command: typing.Iterable[str]) -> DockerfileStage: + """Adds an ENTRYPOINT statement to a Dockerfile stage""" + rpc_args: dict[str, typing.Any] = {'stage': self._handle} + rpc_args['command'] = command + result = self._client.invoke_capability( + 'Aspire.Hosting/entrypoint', + rpc_args, + ) + return typing.cast(DockerfileStage, result) + + def run_with_mounts(self, command: str, mounts: typing.Iterable[str]) -> DockerfileStage: + """Adds a RUN statement with mounts to a Dockerfile stage""" + rpc_args: dict[str, typing.Any] = {'stage': self._handle} + rpc_args['command'] = command + rpc_args['mounts'] = mounts + result = self._client.invoke_capability( + 'Aspire.Hosting/runWithMounts', + rpc_args, + ) + return typing.cast(DockerfileStage, result) + + def user(self, user: str) -> DockerfileStage: + """Adds a USER statement to a Dockerfile stage""" + rpc_args: dict[str, typing.Any] = {'stage': self._handle} + rpc_args['user'] = user + result = self._client.invoke_capability( + 'Aspire.Hosting/user', + rpc_args, + ) + return typing.cast(DockerfileStage, result) + + def empty_line(self) -> DockerfileStage: + """Adds an empty line to a Dockerfile stage""" + rpc_args: dict[str, typing.Any] = {'stage': self._handle} + result = self._client.invoke_capability( + 'Aspire.Hosting/emptyLine', + rpc_args, + ) + return typing.cast(DockerfileStage, result) + + def comment(self, comment: str) -> DockerfileStage: + """Adds a comment to a Dockerfile stage""" + rpc_args: dict[str, typing.Any] = {'stage': self._handle} + rpc_args['comment'] = comment + result = self._client.invoke_capability( + 'Aspire.Hosting/comment', + rpc_args, + ) + return typing.cast(DockerfileStage, result) + + def add_container_files(self, resource: AbstractResource, root_destination_path: str, *, logger: AbstractLogger | None = None) -> DockerfileStage: + """Adds COPY --from statements for published container files""" + rpc_args: dict[str, typing.Any] = {'stage': self._handle} + rpc_args['resource'] = resource + rpc_args['rootDestinationPath'] = root_destination_path + if logger is not None: + rpc_args['logger'] = logger + result = self._client.invoke_capability( + 'Aspire.Hosting/dockerfileStageAddContainerFiles', + rpc_args, + ) + return typing.cast(DockerfileStage, result) + + class EndpointReference: """Type class for EndpointReference.""" @@ -6110,6 +6403,7 @@ class ContainerResourceKwargs(_BaseResourceKwargs, total=False): build_arg: tuple[str, ParameterResource] build_secret: tuple[str, ParameterResource] endpoint_proxy_support: bool + dockerfile_builder: tuple[str, typing.Callable[[DockerfileBuilderCallbackContext], None]] | DockerfileBuilderParameters container_network_alias: str mcp_server: McpServerParameters | typing.Literal[True] otlp_exporter: OtlpProtocol | typing.Literal[True] @@ -6329,6 +6623,20 @@ def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: self._handle = self._wrap_builder(result) return self + def with_dockerfile_builder(self, context_path: str, callback: typing.Callable[[DockerfileBuilderCallbackContext], None], *, stage: str | None = None) -> typing.Self: + """Configures the resource to use a programmatically generated Dockerfile""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['contextPath'] = context_path + rpc_args['callback'] = self._client.register_callback(callback) + if stage is not None: + rpc_args['stage'] = stage + result = self._client.invoke_capability( + 'Aspire.Hosting/withDockerfileBuilder', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + def with_container_network_alias(self, alias: str) -> typing.Self: """Adds a network alias for the container""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -6950,6 +7258,20 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) else: raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") + if _dockerfile_builder := kwargs.pop("dockerfile_builder", None): + if _validate_tuple_types(_dockerfile_builder, (str, typing.Callable[[DockerfileBuilderCallbackContext], None])): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["contextPath"] = typing.cast(tuple[str, typing.Callable[[DockerfileBuilderCallbackContext], None]], _dockerfile_builder)[0] + rpc_args["callback"] = client.register_callback(typing.cast(tuple[str, typing.Callable[[DockerfileBuilderCallbackContext], None]], _dockerfile_builder)[1]) + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withDockerfileBuilder', rpc_args)) + elif _validate_dict_types(_dockerfile_builder, DockerfileBuilderParameters): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["contextPath"] = typing.cast(DockerfileBuilderParameters, _dockerfile_builder)["context_path"] + rpc_args["callback"] = client.register_callback(typing.cast(DockerfileBuilderParameters, _dockerfile_builder)["callback"]) + rpc_args["stage"] = typing.cast(DockerfileBuilderParameters, _dockerfile_builder).get("stage") + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withDockerfileBuilder', rpc_args)) + else: + raise TypeError("Invalid type for option 'dockerfile_builder'. Expected: (str, Callable[[DockerfileBuilderCallbackContext], None]) or DockerfileBuilderParameters") if _container_network_alias := kwargs.pop("container_network_alias", None): if _validate_type(_container_network_alias, str): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -9736,6 +10058,9 @@ def create_builder( _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.Eventing.DistributedApplicationEventSubscription", DistributedApplicationEventSubscription) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.DistributedApplicationExecutionContext", DistributedApplicationExecutionContext) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.DistributedApplicationModel", DistributedApplicationModel) +_register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.Docker.DockerfileBuilder", DockerfileBuilder) +_register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.DockerfileBuilderCallbackContext", DockerfileBuilderCallbackContext) +_register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.Docker.DockerfileStage", DockerfileStage) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.EndpointReference", EndpointReference) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.EndpointReferenceExpression", EndpointReferenceExpression) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.EnvironmentCallbackContext", EnvironmentCallbackContext) diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt index 04910ccc106..7322b51c0f0 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt @@ -433,6 +433,20 @@ } ] }, + { + CapabilityId: Aspire.Hosting/withDockerfileBuilder, + MethodName: withDockerfileBuilder, + TargetType: { + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, + IsInterface: false + }, + ExpandedTargetTypes: [ + { + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, + IsInterface: false + } + ] + }, { CapabilityId: Aspire.Hosting/withEndpoint, MethodName: withEndpoint, diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index 03c76b21116..a7f4b1fac83 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -1,4 +1,4 @@ -// aspire.ts - Capability-based Aspire SDK +// aspire.ts - Capability-based Aspire SDK // This SDK uses the ATS (Aspire Type System) capability API. // Capabilities are endpoints like 'Aspire.Hosting/createBuilder'. // @@ -82,6 +82,15 @@ type CSharpAppResourceHandle = Handle<'Aspire.Hosting/Aspire.Hosting.Application /** Handle to DistributedApplicationModel */ type DistributedApplicationModelHandle = Handle<'Aspire.Hosting/Aspire.Hosting.ApplicationModel.DistributedApplicationModel'>; +/** Handle to DockerfileBuilder */ +type DockerfileBuilderHandle = Handle<'Aspire.Hosting/Aspire.Hosting.ApplicationModel.Docker.DockerfileBuilder'>; + +/** Handle to DockerfileStage */ +type DockerfileStageHandle = Handle<'Aspire.Hosting/Aspire.Hosting.ApplicationModel.Docker.DockerfileStage'>; + +/** Handle to DockerfileBuilderCallbackContext */ +type DockerfileBuilderCallbackContextHandle = Handle<'Aspire.Hosting/Aspire.Hosting.ApplicationModel.DockerfileBuilderCallbackContext'>; + /** Handle to DotnetToolResource */ type DotnetToolResourceHandle = Handle<'Aspire.Hosting/Aspire.Hosting.ApplicationModel.DotnetToolResource'>; @@ -451,6 +460,14 @@ export interface AddConnectionStringOptions { environmentVariableName?: string; } +export interface AddContainerFilesOptions { + logger?: Logger; +} + +export interface AddContainerFilesStagesOptions { + logger?: Logger; +} + export interface AddContainerRegistryFromStringOptions { repository?: string; } @@ -459,6 +476,10 @@ export interface AddContainerRegistryOptions { repository?: ParameterResource; } +export interface AddDockerfileBuilderOptions { + stage?: string; +} + export interface AddDockerfileOptions { dockerfilePath?: string; stage?: string; @@ -493,6 +514,10 @@ export interface AppendValueProviderOptions { format?: string; } +export interface ArgOptions { + defaultValue?: string; +} + export interface CompleteStepMarkdownOptions { completionState?: string; cancellationToken?: AbortSignal | CancellationToken; @@ -514,6 +539,14 @@ export interface CompleteTaskOptions { cancellationToken?: AbortSignal | CancellationToken; } +export interface CopyFromOptions { + chown?: string; +} + +export interface CopyOptions { + chown?: string; +} + export interface CreateMarkdownTaskOptions { cancellationToken?: AbortSignal | CancellationToken; } @@ -522,6 +555,10 @@ export interface CreateTaskOptions { cancellationToken?: AbortSignal | CancellationToken; } +export interface FromOptions { + stageName?: string; +} + export interface GetStatusAsyncOptions { cancellationToken?: AbortSignal | CancellationToken; } @@ -589,6 +626,10 @@ export interface WithDockerfileBaseImageOptions { runtimeImage?: string; } +export interface WithDockerfileBuilderOptions { + stage?: string; +} + export interface WithDockerfileOptions { dockerfilePath?: string; stage?: string; @@ -1264,6 +1305,555 @@ class DistributedApplicationModelPromiseImpl implements DistributedApplicationMo } +// ============================================================================ +// DockerfileBuilder +// ============================================================================ + +export interface DockerfileBuilder { + toJSON(): MarshalledHandle; + arg(name: string, options?: ArgOptions): DockerfileBuilderPromise; + from(image: string, options?: FromOptions): DockerfileStagePromise; + addContainerFilesStages(resource: CSharpAppResource | ComputeResource | ConnectionStringResource | ContainerFilesDestinationResource | ContainerRegistryResource | ContainerResource | DotnetToolResource | ExecutableResource | ExternalServiceResource | ParameterResource | ProjectResource | Resource | ResourceWithArgs | ResourceWithConnectionString | ResourceWithContainerFiles | ResourceWithEndpoints | ResourceWithEnvironment | ResourceWithWaitSupport | TestDatabaseResource | TestRedisResource | TestVaultResource, options?: AddContainerFilesStagesOptions): DockerfileBuilderPromise; +} + +export interface DockerfileBuilderPromise extends PromiseLike { + arg(name: string, options?: ArgOptions): DockerfileBuilderPromise; + from(image: string, options?: FromOptions): DockerfileStagePromise; + addContainerFilesStages(resource: CSharpAppResource | ComputeResource | ConnectionStringResource | ContainerFilesDestinationResource | ContainerRegistryResource | ContainerResource | DotnetToolResource | ExecutableResource | ExternalServiceResource | ParameterResource | ProjectResource | Resource | ResourceWithArgs | ResourceWithConnectionString | ResourceWithContainerFiles | ResourceWithEndpoints | ResourceWithEnvironment | ResourceWithWaitSupport | TestDatabaseResource | TestRedisResource | TestVaultResource, options?: AddContainerFilesStagesOptions): DockerfileBuilderPromise; +} + +// ============================================================================ +// DockerfileBuilderImpl +// ============================================================================ + +/** + * Type class for DockerfileBuilder. + */ +class DockerfileBuilderImpl implements DockerfileBuilder { + constructor(private _handle: DockerfileBuilderHandle, private _client: AspireClientRpc) {} + + /** Serialize for JSON-RPC transport */ + toJSON(): MarshalledHandle { return this._handle.toJSON(); } + + /** Adds a global ARG statement to the Dockerfile */ + /** @internal */ + async _argInternal(name: string, defaultValue?: string): Promise { + const rpcArgs: Record = { builder: this._handle, name }; + if (defaultValue !== undefined) rpcArgs.defaultValue = defaultValue; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/dockerfileBuilderArg', + rpcArgs + ); + return new DockerfileBuilderImpl(result, this._client); + } + + arg(name: string, options?: ArgOptions): DockerfileBuilderPromise { + const defaultValue = options?.defaultValue; + return new DockerfileBuilderPromiseImpl(this._argInternal(name, defaultValue)); + } + + /** Adds a FROM statement to start a Dockerfile stage */ + /** @internal */ + async _fromInternal(image: string, stageName?: string): Promise { + const rpcArgs: Record = { builder: this._handle, image }; + if (stageName !== undefined) rpcArgs.stageName = stageName; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/dockerfileBuilderFrom', + rpcArgs + ); + return new DockerfileStageImpl(result, this._client); + } + + from(image: string, options?: FromOptions): DockerfileStagePromise { + const stageName = options?.stageName; + return new DockerfileStagePromiseImpl(this._fromInternal(image, stageName)); + } + + /** Adds Dockerfile stages for published container files */ + /** @internal */ + async _addContainerFilesStagesInternal(resource: CSharpAppResource | ComputeResource | ConnectionStringResource | ContainerFilesDestinationResource | ContainerRegistryResource | ContainerResource | DotnetToolResource | ExecutableResource | ExternalServiceResource | ParameterResource | ProjectResource | Resource | ResourceWithArgs | ResourceWithConnectionString | ResourceWithContainerFiles | ResourceWithEndpoints | ResourceWithEnvironment | ResourceWithWaitSupport | TestDatabaseResource | TestRedisResource | TestVaultResource, logger?: Logger): Promise { + const rpcArgs: Record = { builder: this._handle, resource }; + if (logger !== undefined) rpcArgs.logger = logger; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/dockerfileBuilderAddContainerFilesStages', + rpcArgs + ); + return new DockerfileBuilderImpl(result, this._client); + } + + addContainerFilesStages(resource: CSharpAppResource | ComputeResource | ConnectionStringResource | ContainerFilesDestinationResource | ContainerRegistryResource | ContainerResource | DotnetToolResource | ExecutableResource | ExternalServiceResource | ParameterResource | ProjectResource | Resource | ResourceWithArgs | ResourceWithConnectionString | ResourceWithContainerFiles | ResourceWithEndpoints | ResourceWithEnvironment | ResourceWithWaitSupport | TestDatabaseResource | TestRedisResource | TestVaultResource, options?: AddContainerFilesStagesOptions): DockerfileBuilderPromise { + const logger = options?.logger; + return new DockerfileBuilderPromiseImpl(this._addContainerFilesStagesInternal(resource, logger)); + } + +} + +/** + * Thenable wrapper for DockerfileBuilder that enables fluent chaining. + */ +class DockerfileBuilderPromiseImpl implements DockerfileBuilderPromise { + constructor(private _promise: Promise) {} + + then( + onfulfilled?: ((value: DockerfileBuilder) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null + ): PromiseLike { + return this._promise.then(onfulfilled, onrejected); + } + + /** Adds a global ARG statement to the Dockerfile */ + arg(name: string, options?: ArgOptions): DockerfileBuilderPromise { + return new DockerfileBuilderPromiseImpl(this._promise.then(obj => obj.arg(name, options))); + } + + /** Adds a FROM statement to start a Dockerfile stage */ + from(image: string, options?: FromOptions): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._promise.then(obj => obj.from(image, options))); + } + + /** Adds Dockerfile stages for published container files */ + addContainerFilesStages(resource: CSharpAppResource | ComputeResource | ConnectionStringResource | ContainerFilesDestinationResource | ContainerRegistryResource | ContainerResource | DotnetToolResource | ExecutableResource | ExternalServiceResource | ParameterResource | ProjectResource | Resource | ResourceWithArgs | ResourceWithConnectionString | ResourceWithContainerFiles | ResourceWithEndpoints | ResourceWithEnvironment | ResourceWithWaitSupport | TestDatabaseResource | TestRedisResource | TestVaultResource, options?: AddContainerFilesStagesOptions): DockerfileBuilderPromise { + return new DockerfileBuilderPromiseImpl(this._promise.then(obj => obj.addContainerFilesStages(resource, options))); + } + +} + +// ============================================================================ +// DockerfileBuilderCallbackContext +// ============================================================================ + +export interface DockerfileBuilderCallbackContext { + toJSON(): MarshalledHandle; + resource: { + get: () => Promise; + }; + builder: { + get: () => Promise; + }; + services: { + get: () => Promise; + }; + cancellationToken: { + get: () => Promise; + }; +} + +// ============================================================================ +// DockerfileBuilderCallbackContextImpl +// ============================================================================ + +/** + * Type class for DockerfileBuilderCallbackContext. + */ +class DockerfileBuilderCallbackContextImpl implements DockerfileBuilderCallbackContext { + constructor(private _handle: DockerfileBuilderCallbackContextHandle, private _client: AspireClientRpc) {} + + /** Serialize for JSON-RPC transport */ + toJSON(): MarshalledHandle { return this._handle.toJSON(); } + + /** Gets the Resource property */ + resource = { + get: async (): Promise => { + const handle = await this._client.invokeCapability( + 'Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.resource', + { context: this._handle } + ); + return new ResourceImpl(handle, this._client); + }, + }; + + /** Gets the Builder property */ + builder = { + get: async (): Promise => { + const handle = await this._client.invokeCapability( + 'Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.builder', + { context: this._handle } + ); + return new DockerfileBuilderImpl(handle, this._client); + }, + }; + + /** Gets the Services property */ + services = { + get: async (): Promise => { + const handle = await this._client.invokeCapability( + 'Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.services', + { context: this._handle } + ); + return new ServiceProviderImpl(handle, this._client); + }, + }; + + /** Gets the CancellationToken property */ + cancellationToken = { + get: async (): Promise => { + const result = await this._client.invokeCapability( + 'Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.cancellationToken', + { context: this._handle } + ); + return CancellationToken.fromValue(result); + }, + }; + +} + +// ============================================================================ +// DockerfileStage +// ============================================================================ + +export interface DockerfileStage { + toJSON(): MarshalledHandle; + arg(name: string, options?: ArgOptions): DockerfileStagePromise; + workDir(path: string): DockerfileStagePromise; + run(command: string): DockerfileStagePromise; + copy(source: string, destination: string, options?: CopyOptions): DockerfileStagePromise; + copyFrom(from: string, source: string, destination: string, options?: CopyFromOptions): DockerfileStagePromise; + env(name: string, value: string): DockerfileStagePromise; + expose(port: number): DockerfileStagePromise; + cmd(command: string[]): DockerfileStagePromise; + entrypoint(command: string[]): DockerfileStagePromise; + runWithMounts(command: string, mounts: string[]): DockerfileStagePromise; + user(user: string): DockerfileStagePromise; + emptyLine(): DockerfileStagePromise; + comment(comment: string): DockerfileStagePromise; + addContainerFiles(resource: CSharpAppResource | ComputeResource | ConnectionStringResource | ContainerFilesDestinationResource | ContainerRegistryResource | ContainerResource | DotnetToolResource | ExecutableResource | ExternalServiceResource | ParameterResource | ProjectResource | Resource | ResourceWithArgs | ResourceWithConnectionString | ResourceWithContainerFiles | ResourceWithEndpoints | ResourceWithEnvironment | ResourceWithWaitSupport | TestDatabaseResource | TestRedisResource | TestVaultResource, rootDestinationPath: string, options?: AddContainerFilesOptions): DockerfileStagePromise; +} + +export interface DockerfileStagePromise extends PromiseLike { + arg(name: string, options?: ArgOptions): DockerfileStagePromise; + workDir(path: string): DockerfileStagePromise; + run(command: string): DockerfileStagePromise; + copy(source: string, destination: string, options?: CopyOptions): DockerfileStagePromise; + copyFrom(from: string, source: string, destination: string, options?: CopyFromOptions): DockerfileStagePromise; + env(name: string, value: string): DockerfileStagePromise; + expose(port: number): DockerfileStagePromise; + cmd(command: string[]): DockerfileStagePromise; + entrypoint(command: string[]): DockerfileStagePromise; + runWithMounts(command: string, mounts: string[]): DockerfileStagePromise; + user(user: string): DockerfileStagePromise; + emptyLine(): DockerfileStagePromise; + comment(comment: string): DockerfileStagePromise; + addContainerFiles(resource: CSharpAppResource | ComputeResource | ConnectionStringResource | ContainerFilesDestinationResource | ContainerRegistryResource | ContainerResource | DotnetToolResource | ExecutableResource | ExternalServiceResource | ParameterResource | ProjectResource | Resource | ResourceWithArgs | ResourceWithConnectionString | ResourceWithContainerFiles | ResourceWithEndpoints | ResourceWithEnvironment | ResourceWithWaitSupport | TestDatabaseResource | TestRedisResource | TestVaultResource, rootDestinationPath: string, options?: AddContainerFilesOptions): DockerfileStagePromise; +} + +// ============================================================================ +// DockerfileStageImpl +// ============================================================================ + +/** + * Type class for DockerfileStage. + */ +class DockerfileStageImpl implements DockerfileStage { + constructor(private _handle: DockerfileStageHandle, private _client: AspireClientRpc) {} + + /** Serialize for JSON-RPC transport */ + toJSON(): MarshalledHandle { return this._handle.toJSON(); } + + /** Adds an ARG statement to a Dockerfile stage */ + /** @internal */ + async _argInternal(name: string, defaultValue?: string): Promise { + const rpcArgs: Record = { stage: this._handle, name }; + if (defaultValue !== undefined) rpcArgs.defaultValue = defaultValue; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/dockerfileStageArg', + rpcArgs + ); + return new DockerfileStageImpl(result, this._client); + } + + arg(name: string, options?: ArgOptions): DockerfileStagePromise { + const defaultValue = options?.defaultValue; + return new DockerfileStagePromiseImpl(this._argInternal(name, defaultValue)); + } + + /** Adds a WORKDIR statement to a Dockerfile stage */ + /** @internal */ + async _workDirInternal(path: string): Promise { + const rpcArgs: Record = { stage: this._handle, path }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/workDir', + rpcArgs + ); + return new DockerfileStageImpl(result, this._client); + } + + workDir(path: string): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._workDirInternal(path)); + } + + /** Adds a RUN statement to a Dockerfile stage */ + /** @internal */ + async _runInternal(command: string): Promise { + const rpcArgs: Record = { stage: this._handle, command }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/dockerfileStageRun', + rpcArgs + ); + return new DockerfileStageImpl(result, this._client); + } + + run(command: string): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._runInternal(command)); + } + + /** Adds a COPY statement to a Dockerfile stage */ + /** @internal */ + async _copyInternal(source: string, destination: string, chown?: string): Promise { + const rpcArgs: Record = { stage: this._handle, source, destination }; + if (chown !== undefined) rpcArgs.chown = chown; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/dockerfileStageCopy', + rpcArgs + ); + return new DockerfileStageImpl(result, this._client); + } + + copy(source: string, destination: string, options?: CopyOptions): DockerfileStagePromise { + const chown = options?.chown; + return new DockerfileStagePromiseImpl(this._copyInternal(source, destination, chown)); + } + + /** Adds a COPY --from statement to a Dockerfile stage */ + /** @internal */ + async _copyFromInternal(from: string, source: string, destination: string, chown?: string): Promise { + const rpcArgs: Record = { stage: this._handle, from, source, destination }; + if (chown !== undefined) rpcArgs.chown = chown; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/dockerfileStageCopyFrom', + rpcArgs + ); + return new DockerfileStageImpl(result, this._client); + } + + copyFrom(from: string, source: string, destination: string, options?: CopyFromOptions): DockerfileStagePromise { + const chown = options?.chown; + return new DockerfileStagePromiseImpl(this._copyFromInternal(from, source, destination, chown)); + } + + /** Adds an ENV statement to a Dockerfile stage */ + /** @internal */ + async _envInternal(name: string, value: string): Promise { + const rpcArgs: Record = { stage: this._handle, name, value }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/env', + rpcArgs + ); + return new DockerfileStageImpl(result, this._client); + } + + env(name: string, value: string): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._envInternal(name, value)); + } + + /** Adds an EXPOSE statement to a Dockerfile stage */ + /** @internal */ + async _exposeInternal(port: number): Promise { + const rpcArgs: Record = { stage: this._handle, port }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/expose', + rpcArgs + ); + return new DockerfileStageImpl(result, this._client); + } + + expose(port: number): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._exposeInternal(port)); + } + + /** Adds a CMD statement to a Dockerfile stage */ + /** @internal */ + async _cmdInternal(command: string[]): Promise { + const rpcArgs: Record = { stage: this._handle, command }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/cmd', + rpcArgs + ); + return new DockerfileStageImpl(result, this._client); + } + + cmd(command: string[]): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._cmdInternal(command)); + } + + /** Adds an ENTRYPOINT statement to a Dockerfile stage */ + /** @internal */ + async _entrypointInternal(command: string[]): Promise { + const rpcArgs: Record = { stage: this._handle, command }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/entrypoint', + rpcArgs + ); + return new DockerfileStageImpl(result, this._client); + } + + entrypoint(command: string[]): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._entrypointInternal(command)); + } + + /** Adds a RUN statement with mounts to a Dockerfile stage */ + /** @internal */ + async _runWithMountsInternal(command: string, mounts: string[]): Promise { + const rpcArgs: Record = { stage: this._handle, command, mounts }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/runWithMounts', + rpcArgs + ); + return new DockerfileStageImpl(result, this._client); + } + + runWithMounts(command: string, mounts: string[]): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._runWithMountsInternal(command, mounts)); + } + + /** Adds a USER statement to a Dockerfile stage */ + /** @internal */ + async _userInternal(user: string): Promise { + const rpcArgs: Record = { stage: this._handle, user }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/user', + rpcArgs + ); + return new DockerfileStageImpl(result, this._client); + } + + user(user: string): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._userInternal(user)); + } + + /** Adds an empty line to a Dockerfile stage */ + /** @internal */ + async _emptyLineInternal(): Promise { + const rpcArgs: Record = { stage: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/emptyLine', + rpcArgs + ); + return new DockerfileStageImpl(result, this._client); + } + + emptyLine(): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._emptyLineInternal()); + } + + /** Adds a comment to a Dockerfile stage */ + /** @internal */ + async _commentInternal(comment: string): Promise { + const rpcArgs: Record = { stage: this._handle, comment }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/comment', + rpcArgs + ); + return new DockerfileStageImpl(result, this._client); + } + + comment(comment: string): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._commentInternal(comment)); + } + + /** Adds COPY --from statements for published container files */ + /** @internal */ + async _addContainerFilesInternal(resource: CSharpAppResource | ComputeResource | ConnectionStringResource | ContainerFilesDestinationResource | ContainerRegistryResource | ContainerResource | DotnetToolResource | ExecutableResource | ExternalServiceResource | ParameterResource | ProjectResource | Resource | ResourceWithArgs | ResourceWithConnectionString | ResourceWithContainerFiles | ResourceWithEndpoints | ResourceWithEnvironment | ResourceWithWaitSupport | TestDatabaseResource | TestRedisResource | TestVaultResource, rootDestinationPath: string, logger?: Logger): Promise { + const rpcArgs: Record = { stage: this._handle, resource, rootDestinationPath }; + if (logger !== undefined) rpcArgs.logger = logger; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/dockerfileStageAddContainerFiles', + rpcArgs + ); + return new DockerfileStageImpl(result, this._client); + } + + addContainerFiles(resource: CSharpAppResource | ComputeResource | ConnectionStringResource | ContainerFilesDestinationResource | ContainerRegistryResource | ContainerResource | DotnetToolResource | ExecutableResource | ExternalServiceResource | ParameterResource | ProjectResource | Resource | ResourceWithArgs | ResourceWithConnectionString | ResourceWithContainerFiles | ResourceWithEndpoints | ResourceWithEnvironment | ResourceWithWaitSupport | TestDatabaseResource | TestRedisResource | TestVaultResource, rootDestinationPath: string, options?: AddContainerFilesOptions): DockerfileStagePromise { + const logger = options?.logger; + return new DockerfileStagePromiseImpl(this._addContainerFilesInternal(resource, rootDestinationPath, logger)); + } + +} + +/** + * Thenable wrapper for DockerfileStage that enables fluent chaining. + */ +class DockerfileStagePromiseImpl implements DockerfileStagePromise { + constructor(private _promise: Promise) {} + + then( + onfulfilled?: ((value: DockerfileStage) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null + ): PromiseLike { + return this._promise.then(onfulfilled, onrejected); + } + + /** Adds an ARG statement to a Dockerfile stage */ + arg(name: string, options?: ArgOptions): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._promise.then(obj => obj.arg(name, options))); + } + + /** Adds a WORKDIR statement to a Dockerfile stage */ + workDir(path: string): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._promise.then(obj => obj.workDir(path))); + } + + /** Adds a RUN statement to a Dockerfile stage */ + run(command: string): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._promise.then(obj => obj.run(command))); + } + + /** Adds a COPY statement to a Dockerfile stage */ + copy(source: string, destination: string, options?: CopyOptions): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._promise.then(obj => obj.copy(source, destination, options))); + } + + /** Adds a COPY --from statement to a Dockerfile stage */ + copyFrom(from: string, source: string, destination: string, options?: CopyFromOptions): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._promise.then(obj => obj.copyFrom(from, source, destination, options))); + } + + /** Adds an ENV statement to a Dockerfile stage */ + env(name: string, value: string): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._promise.then(obj => obj.env(name, value))); + } + + /** Adds an EXPOSE statement to a Dockerfile stage */ + expose(port: number): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._promise.then(obj => obj.expose(port))); + } + + /** Adds a CMD statement to a Dockerfile stage */ + cmd(command: string[]): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._promise.then(obj => obj.cmd(command))); + } + + /** Adds an ENTRYPOINT statement to a Dockerfile stage */ + entrypoint(command: string[]): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._promise.then(obj => obj.entrypoint(command))); + } + + /** Adds a RUN statement with mounts to a Dockerfile stage */ + runWithMounts(command: string, mounts: string[]): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._promise.then(obj => obj.runWithMounts(command, mounts))); + } + + /** Adds a USER statement to a Dockerfile stage */ + user(user: string): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._promise.then(obj => obj.user(user))); + } + + /** Adds an empty line to a Dockerfile stage */ + emptyLine(): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._promise.then(obj => obj.emptyLine())); + } + + /** Adds a comment to a Dockerfile stage */ + comment(comment: string): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._promise.then(obj => obj.comment(comment))); + } + + /** Adds COPY --from statements for published container files */ + addContainerFiles(resource: CSharpAppResource | ComputeResource | ConnectionStringResource | ContainerFilesDestinationResource | ContainerRegistryResource | ContainerResource | DotnetToolResource | ExecutableResource | ExternalServiceResource | ParameterResource | ProjectResource | Resource | ResourceWithArgs | ResourceWithConnectionString | ResourceWithContainerFiles | ResourceWithEndpoints | ResourceWithEnvironment | ResourceWithWaitSupport | TestDatabaseResource | TestRedisResource | TestVaultResource, rootDestinationPath: string, options?: AddContainerFilesOptions): DockerfileStagePromise { + return new DockerfileStagePromiseImpl(this._promise.then(obj => obj.addContainerFiles(resource, rootDestinationPath, options))); + } + +} + // ============================================================================ // EndpointReference // ============================================================================ @@ -3886,6 +4476,7 @@ export interface DistributedApplicationBuilder { addContainerRegistryFromString(name: string, endpoint: string, options?: AddContainerRegistryFromStringOptions): ContainerRegistryResourcePromise; addContainer(name: string, image: string): ContainerResourcePromise; addDockerfile(name: string, contextPath: string, options?: AddDockerfileOptions): ContainerResourcePromise; + addDockerfileBuilder(name: string, contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: AddDockerfileBuilderOptions): ContainerResourcePromise; addDotnetTool(name: string, packageId: string): DotnetToolResourcePromise; addExecutable(name: string, command: string, workingDirectory: string, args: string[]): ExecutableResourcePromise; addExternalService(name: string, url: string): ExternalServiceResourcePromise; @@ -3914,6 +4505,7 @@ export interface DistributedApplicationBuilderPromise extends PromiseLike Promise, options?: AddDockerfileBuilderOptions): ContainerResourcePromise; addDotnetTool(name: string, packageId: string): DotnetToolResourcePromise; addExecutable(name: string, command: string, workingDirectory: string, args: string[]): ExecutableResourcePromise; addExternalService(name: string, url: string): ExternalServiceResourcePromise; @@ -4119,6 +4711,28 @@ class DistributedApplicationBuilderImpl implements DistributedApplicationBuilder return new ContainerResourcePromiseImpl(this._addDockerfileInternal(name, contextPath, dockerfilePath, stage)); } + /** Adds a container resource built from a programmatically generated Dockerfile */ + /** @internal */ + async _addDockerfileBuilderInternal(name: string, contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, stage?: string): Promise { + const callbackId = registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as DockerfileBuilderCallbackContextHandle; + const arg = new DockerfileBuilderCallbackContextImpl(argHandle, this._client); + await callback(arg); + }); + const rpcArgs: Record = { builder: this._handle, name, contextPath, callback: callbackId }; + if (stage !== undefined) rpcArgs.stage = stage; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/addDockerfileBuilder', + rpcArgs + ); + return new ContainerResourceImpl(result, this._client); + } + + addDockerfileBuilder(name: string, contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: AddDockerfileBuilderOptions): ContainerResourcePromise { + const stage = options?.stage; + return new ContainerResourcePromiseImpl(this._addDockerfileBuilderInternal(name, contextPath, callback, stage)); + } + /** Adds a .NET tool resource */ /** @internal */ async _addDotnetToolInternal(name: string, packageId: string): Promise { @@ -4459,6 +5073,11 @@ class DistributedApplicationBuilderPromiseImpl implements DistributedApplication return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.addDockerfile(name, contextPath, options))); } + /** Adds a container resource built from a programmatically generated Dockerfile */ + addDockerfileBuilder(name: string, contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: AddDockerfileBuilderOptions): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.addDockerfileBuilder(name, contextPath, callback, options))); + } + /** Adds a .NET tool resource */ addDotnetTool(name: string, packageId: string): DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.addDotnetTool(name, packageId))); @@ -8018,6 +8637,7 @@ export interface ContainerResource { withBuildArg(name: string, value: ParameterResource): ContainerResourcePromise; withBuildSecret(name: string, value: ParameterResource): ContainerResourcePromise; withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; + withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): ContainerResourcePromise; withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): ContainerResourcePromise; withContainerNetworkAlias(alias: string): ContainerResourcePromise; withMcpServer(options?: WithMcpServerOptions): ContainerResourcePromise; @@ -8121,6 +8741,7 @@ export interface ContainerResourcePromise extends PromiseLike withBuildArg(name: string, value: ParameterResource): ContainerResourcePromise; withBuildSecret(name: string, value: ParameterResource): ContainerResourcePromise; withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; + withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): ContainerResourcePromise; withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): ContainerResourcePromise; withContainerNetworkAlias(alias: string): ContainerResourcePromise; withMcpServer(options?: WithMcpServerOptions): ContainerResourcePromise; @@ -8464,6 +9085,28 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled)); } + /** @internal */ + private async _withDockerfileBuilderInternal(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, stage?: string): Promise { + const callbackId = registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as DockerfileBuilderCallbackContextHandle; + const arg = new DockerfileBuilderCallbackContextImpl(argHandle, this._client); + await callback(arg); + }); + const rpcArgs: Record = { builder: this._handle, contextPath, callback: callbackId }; + if (stage !== undefined) rpcArgs.stage = stage; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withDockerfileBuilder', + rpcArgs + ); + return new ContainerResourceImpl(result, this._client); + } + + /** Configures the resource to use a programmatically generated Dockerfile */ + withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): ContainerResourcePromise { + const stage = options?.stage; + return new ContainerResourcePromiseImpl(this._withDockerfileBuilderInternal(contextPath, callback, stage)); + } + /** @internal */ private async _withDockerfileBaseImageInternal(buildImage?: string, runtimeImage?: string): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -10014,6 +10657,11 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled))); } + /** Configures the resource to use a programmatically generated Dockerfile */ + withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBuilder(contextPath, callback, options))); + } + /** Sets the base image for a Dockerfile build */ withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBaseImage(options))); @@ -21394,6 +22042,7 @@ export interface TestDatabaseResource { withBuildArg(name: string, value: ParameterResource): TestDatabaseResourcePromise; withBuildSecret(name: string, value: ParameterResource): TestDatabaseResourcePromise; withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise; + withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): TestDatabaseResourcePromise; withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): TestDatabaseResourcePromise; withContainerNetworkAlias(alias: string): TestDatabaseResourcePromise; withMcpServer(options?: WithMcpServerOptions): TestDatabaseResourcePromise; @@ -21497,6 +22146,7 @@ export interface TestDatabaseResourcePromise extends PromiseLike Promise, options?: WithDockerfileBuilderOptions): TestDatabaseResourcePromise; withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): TestDatabaseResourcePromise; withContainerNetworkAlias(alias: string): TestDatabaseResourcePromise; withMcpServer(options?: WithMcpServerOptions): TestDatabaseResourcePromise; @@ -21840,6 +22490,28 @@ class TestDatabaseResourceImpl extends ResourceBuilderBase Promise, stage?: string): Promise { + const callbackId = registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as DockerfileBuilderCallbackContextHandle; + const arg = new DockerfileBuilderCallbackContextImpl(argHandle, this._client); + await callback(arg); + }); + const rpcArgs: Record = { builder: this._handle, contextPath, callback: callbackId }; + if (stage !== undefined) rpcArgs.stage = stage; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withDockerfileBuilder', + rpcArgs + ); + return new TestDatabaseResourceImpl(result, this._client); + } + + /** Configures the resource to use a programmatically generated Dockerfile */ + withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): TestDatabaseResourcePromise { + const stage = options?.stage; + return new TestDatabaseResourcePromiseImpl(this._withDockerfileBuilderInternal(contextPath, callback, stage)); + } + /** @internal */ private async _withDockerfileBaseImageInternal(buildImage?: string, runtimeImage?: string): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -23390,6 +24062,11 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled))); } + /** Configures the resource to use a programmatically generated Dockerfile */ + withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBuilder(contextPath, callback, options))); + } + /** Sets the base image for a Dockerfile build */ withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBaseImage(options))); @@ -23846,6 +24523,7 @@ export interface TestRedisResource { withBuildArg(name: string, value: ParameterResource): TestRedisResourcePromise; withBuildSecret(name: string, value: ParameterResource): TestRedisResourcePromise; withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; + withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): TestRedisResourcePromise; withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): TestRedisResourcePromise; withContainerNetworkAlias(alias: string): TestRedisResourcePromise; withMcpServer(options?: WithMcpServerOptions): TestRedisResourcePromise; @@ -23965,6 +24643,7 @@ export interface TestRedisResourcePromise extends PromiseLike withBuildArg(name: string, value: ParameterResource): TestRedisResourcePromise; withBuildSecret(name: string, value: ParameterResource): TestRedisResourcePromise; withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; + withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): TestRedisResourcePromise; withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): TestRedisResourcePromise; withContainerNetworkAlias(alias: string): TestRedisResourcePromise; withMcpServer(options?: WithMcpServerOptions): TestRedisResourcePromise; @@ -24324,6 +25003,28 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled)); } + /** @internal */ + private async _withDockerfileBuilderInternal(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, stage?: string): Promise { + const callbackId = registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as DockerfileBuilderCallbackContextHandle; + const arg = new DockerfileBuilderCallbackContextImpl(argHandle, this._client); + await callback(arg); + }); + const rpcArgs: Record = { builder: this._handle, contextPath, callback: callbackId }; + if (stage !== undefined) rpcArgs.stage = stage; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withDockerfileBuilder', + rpcArgs + ); + return new TestRedisResourceImpl(result, this._client); + } + + /** Configures the resource to use a programmatically generated Dockerfile */ + withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): TestRedisResourcePromise { + const stage = options?.stage; + return new TestRedisResourcePromiseImpl(this._withDockerfileBuilderInternal(contextPath, callback, stage)); + } + /** @internal */ private async _withDockerfileBaseImageInternal(buildImage?: string, runtimeImage?: string): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -26102,6 +26803,11 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled))); } + /** Configures the resource to use a programmatically generated Dockerfile */ + withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBuilder(contextPath, callback, options))); + } + /** Sets the base image for a Dockerfile build */ withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBaseImage(options))); @@ -26638,6 +27344,7 @@ export interface TestVaultResource { withBuildArg(name: string, value: ParameterResource): TestVaultResourcePromise; withBuildSecret(name: string, value: ParameterResource): TestVaultResourcePromise; withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; + withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): TestVaultResourcePromise; withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): TestVaultResourcePromise; withContainerNetworkAlias(alias: string): TestVaultResourcePromise; withMcpServer(options?: WithMcpServerOptions): TestVaultResourcePromise; @@ -26742,6 +27449,7 @@ export interface TestVaultResourcePromise extends PromiseLike withBuildArg(name: string, value: ParameterResource): TestVaultResourcePromise; withBuildSecret(name: string, value: ParameterResource): TestVaultResourcePromise; withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; + withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): TestVaultResourcePromise; withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): TestVaultResourcePromise; withContainerNetworkAlias(alias: string): TestVaultResourcePromise; withMcpServer(options?: WithMcpServerOptions): TestVaultResourcePromise; @@ -27086,6 +27794,28 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled)); } + /** @internal */ + private async _withDockerfileBuilderInternal(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, stage?: string): Promise { + const callbackId = registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as DockerfileBuilderCallbackContextHandle; + const arg = new DockerfileBuilderCallbackContextImpl(argHandle, this._client); + await callback(arg); + }); + const rpcArgs: Record = { builder: this._handle, contextPath, callback: callbackId }; + if (stage !== undefined) rpcArgs.stage = stage; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withDockerfileBuilder', + rpcArgs + ); + return new TestVaultResourceImpl(result, this._client); + } + + /** Configures the resource to use a programmatically generated Dockerfile */ + withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): TestVaultResourcePromise { + const stage = options?.stage; + return new TestVaultResourcePromiseImpl(this._withDockerfileBuilderInternal(contextPath, callback, stage)); + } + /** @internal */ private async _withDockerfileBaseImageInternal(buildImage?: string, runtimeImage?: string): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -28651,6 +29381,11 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled))); } + /** Configures the resource to use a programmatically generated Dockerfile */ + withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBuilder(contextPath, callback, options))); + } + /** Sets the base image for a Dockerfile build */ withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBaseImage(options))); @@ -31764,6 +32499,9 @@ registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.Connection registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.DistributedApplication', (handle, client) => new DistributedApplicationImpl(handle as DistributedApplicationHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.DistributedApplicationExecutionContext', (handle, client) => new DistributedApplicationExecutionContextImpl(handle as DistributedApplicationExecutionContextHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.DistributedApplicationModel', (handle, client) => new DistributedApplicationModelImpl(handle as DistributedApplicationModelHandle, client)); +registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.Docker.DockerfileBuilder', (handle, client) => new DockerfileBuilderImpl(handle as DockerfileBuilderHandle, client)); +registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.DockerfileBuilderCallbackContext', (handle, client) => new DockerfileBuilderCallbackContextImpl(handle as DockerfileBuilderCallbackContextHandle, client)); +registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.Docker.DockerfileStage', (handle, client) => new DockerfileStageImpl(handle as DockerfileStageHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.EndpointReference', (handle, client) => new EndpointReferenceImpl(handle as EndpointReferenceHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.EndpointReferenceExpression', (handle, client) => new EndpointReferenceExpressionImpl(handle as EndpointReferenceExpressionHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.EnvironmentCallbackContext', (handle, client) => new EnvironmentCallbackContextImpl(handle as EnvironmentCallbackContextHandle, client)); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java index dc0b4f3e247..f2b8c1175fa 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java @@ -4,6 +4,34 @@ void main() throws Exception { var builder = DistributedApplication.CreateBuilder(); var container = builder.addContainer("mycontainer", "nginx"); var dockerContainer = builder.addDockerfile("dockerapp", "./app"); + var configureDockerfileBuilder = (AspireAction1) (dockerfileContext) -> { + var _dockerfileResource = dockerfileContext.resource(); + var dockerfileServices = dockerfileContext.services(); + var dockerfileLoggerFactory = dockerfileServices.getLoggerFactory(); + var dockerfileLogger = dockerfileLoggerFactory.createLogger("ValidationAppHost.DockerfileBuilder"); + dockerfileLogger.logInformation("Configuring Dockerfile builder"); + var dockerfileBuilder = dockerfileContext.builder(); + dockerfileBuilder.arg("BASE_IMAGE", "mcr.microsoft.com/dotnet/runtime:8.0"); + dockerfileBuilder.addContainerFilesStages(_dockerfileResource, dockerfileLogger); + var buildStage = dockerfileBuilder.from("mcr.microsoft.com/dotnet/sdk:8.0", "build"); + buildStage.arg("APP_DIR", "/src"); + buildStage.workDir("/src"); + buildStage.copy("./src", "/src", "1000:1000"); + buildStage.run("echo building dockerfile"); + buildStage.runWithMounts("echo mounted", new String[] { "/tmp:/mnt" }); + var runtimeStage = dockerfileBuilder.from("mcr.microsoft.com/dotnet/runtime:8.0", "runtime"); + runtimeStage.copyFrom("build", "/src", "/app", "1000:1000"); + runtimeStage.env("ASPIRE_ENV", "Development"); + runtimeStage.expose(8080.0); + runtimeStage.cmd(new String[] { "dotnet", "App.dll" }); + runtimeStage.entrypoint(new String[] { "dotnet", "App.dll" }); + runtimeStage.user("1000"); + runtimeStage.addContainerFiles(_dockerfileResource, "/app", dockerfileLogger); + runtimeStage.emptyLine(); + runtimeStage.comment("Generated by Java polyglot validation"); + }; + var dockerBuilderContainer = builder.addDockerfileBuilder("dockerbuilderapp", "./app", configureDockerfileBuilder, "runtime"); + dockerContainer.withDockerfileBuilder("./app", configureDockerfileBuilder, "runtime"); var exe = builder.addExecutable("myexe", "echo", ".", new String[] { "hello" }); var project = builder.addProject("myproject", "./src/MyProject", "https"); var csharpApp = builder.addCSharpApp("csharpapp", "./src/CSharpApp"); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py index 6517d69641b..d8a894e0acb 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py @@ -5,10 +5,40 @@ with create_builder() as builder: + def configure_dockerfile_builder(dockerfile_context): + _dockerfile_resource = dockerfile_context.resource + _dockerfile_services = dockerfile_context.services + dockerfile_logger_factory = _dockerfile_services.get_logger_factory() + dockerfile_logger = dockerfile_logger_factory.create_logger("ValidationAppHost.DockerfileBuilder") + dockerfile_logger.log_information("Configuring Dockerfile builder") + dockerfile_builder = dockerfile_context.builder + dockerfile_builder.arg("BASE_IMAGE", default_value="mcr.microsoft.com/dotnet/runtime:8.0") + dockerfile_builder.add_container_files_stages(_dockerfile_resource, logger=dockerfile_logger) + + build_stage = dockerfile_builder.from_("mcr.microsoft.com/dotnet/sdk:8.0", stage_name="build") + build_stage.arg("APP_DIR", default_value="/src") + build_stage.work_dir("/src") + build_stage.copy("./src", "/src", chown="1000:1000") + build_stage.run("echo building dockerfile") + build_stage.run_with_mounts("echo mounted", ["/tmp:/mnt"]) + + runtime_stage = dockerfile_builder.from_("mcr.microsoft.com/dotnet/runtime:8.0", stage_name="runtime") + runtime_stage.copy_from("build", "/src", "/app", chown="1000:1000") + runtime_stage.env("ASPIRE_ENV", "Development") + runtime_stage.expose(8080) + runtime_stage.cmd(["dotnet", "App.dll"]) + runtime_stage.entrypoint(["dotnet", "App.dll"]) + runtime_stage.user("1000") + runtime_stage.add_container_files(_dockerfile_resource, "/app", logger=dockerfile_logger) + runtime_stage.empty_line() + runtime_stage.comment("Generated by Python polyglot validation") + # addContainer (pre-existing) container = builder.add_container("resource", "image") # addDockerfile docker_container = builder.add_dockerfile("resource", ".") + docker_builder_container = builder.add_dockerfile_builder("builder-resource", ".", configure_dockerfile_builder, stage="runtime") + docker_container.with_dockerfile_builder(".", configure_dockerfile_builder, stage="runtime") # addExecutable (pre-existing) exe = builder.add_executable("resource", "echo", ".", []) # addProject (pre-existing) diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.ts b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.ts index 5275ce502b4..52f25908224 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.ts +++ b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.ts @@ -5,6 +5,7 @@ import { ProbeType, refExpr, } from './.modules/aspire.js'; +import type { DockerfileBuilderCallbackContext } from './.modules/aspire.js'; const builder = await createBuilder(); @@ -18,6 +19,39 @@ const container = await builder.addContainer("mycontainer", "nginx"); // addDockerfile const dockerContainer = await builder.addDockerfile("dockerapp", "./app"); +const configureDockerfileBuilder = async (dockerfileContext: DockerfileBuilderCallbackContext) => { + const _dockerfileResource = await dockerfileContext.resource.get(); + const dockerfileServices = await dockerfileContext.services.get(); + const dockerfileLoggerFactory = await dockerfileServices.getLoggerFactory(); + const dockerfileLogger = await dockerfileLoggerFactory.createLogger("ValidationAppHost.DockerfileBuilder"); + await dockerfileLogger.logInformation("Configuring Dockerfile builder"); + + const dockerfileBuilder = await dockerfileContext.builder.get(); + await dockerfileBuilder.arg("BASE_IMAGE", { defaultValue: "mcr.microsoft.com/dotnet/runtime:8.0" }); + await dockerfileBuilder.addContainerFilesStages(_dockerfileResource, { logger: dockerfileLogger }); + + const buildStage = await dockerfileBuilder.from("mcr.microsoft.com/dotnet/sdk:8.0", { stageName: "build" }); + await buildStage.arg("APP_DIR", { defaultValue: "/src" }); + await buildStage.workDir("/src"); + await buildStage.copy("./src", "/src", { chown: "1000:1000" }); + await buildStage.run("echo building dockerfile"); + await buildStage.runWithMounts("echo mounted", ["/tmp:/mnt"]); + + const runtimeStage = await dockerfileBuilder.from("mcr.microsoft.com/dotnet/runtime:8.0", { stageName: "runtime" }); + await runtimeStage.copyFrom("build", "/src", "/app", { chown: "1000:1000" }); + await runtimeStage.env("ASPIRE_ENV", "Development"); + await runtimeStage.expose(8080); + await runtimeStage.cmd(["dotnet", "App.dll"]); + await runtimeStage.entrypoint(["dotnet", "App.dll"]); + await runtimeStage.user("1000"); + await runtimeStage.addContainerFiles(_dockerfileResource, "/app", { logger: dockerfileLogger }); + await runtimeStage.emptyLine(); + await runtimeStage.comment("Generated by TypeScript polyglot validation"); +}; + +const dockerBuilderContainer = await builder.addDockerfileBuilder("dockerbuilderapp", "./app", configureDockerfileBuilder, { stage: "runtime" }); +await dockerContainer.withDockerfileBuilder("./app", configureDockerfileBuilder, { stage: "runtime" }); + // addExecutable (pre-existing) const exe = await builder.addExecutable("myexe", "echo", ".", ["hello"]); From 014b24e7f82623e6460fab2dbad47b0361f4916d Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Fri, 3 Apr 2026 16:04:38 -0700 Subject: [PATCH 2/3] Refresh Go and Rust codegen snapshots Update the Go and Rust two-pass generated Aspire snapshots after the Dockerfile builder polyglot export changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...TwoPassScanningGeneratedAspire.verified.go | 438 +++++++++++++++++- ...TwoPassScanningGeneratedAspire.verified.rs | 388 +++++++++++++++- 2 files changed, 824 insertions(+), 2 deletions(-) diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index 7890993632f..07e8ebdd620 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -1,4 +1,4 @@ -// aspire.go - Capability-based Aspire SDK +// aspire.go - Capability-based Aspire SDK // GENERATED CODE - DO NOT EDIT package aspire @@ -3624,6 +3624,25 @@ func (s *ContainerResource) WithEndpointProxySupport(proxyEnabled bool) (*Contai return result.(*ContainerResource), nil } +// WithDockerfileBuilder configures the resource to use a programmatically generated Dockerfile +func (s *ContainerResource) WithDockerfileBuilder(contextPath string, callback func(...any) any, stage *string) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["contextPath"] = SerializeValue(contextPath) + if callback != nil { + reqArgs["callback"] = RegisterCallback(callback) + } + if stage != nil { + reqArgs["stage"] = SerializeValue(stage) + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/withDockerfileBuilder", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + // WithDockerfileBaseImage sets the base image for a Dockerfile build func (s *ContainerResource) WithDockerfileBaseImage(buildImage *string, runtimeImage *string) (*IResource, error) { reqArgs := map[string]any{ @@ -5102,6 +5121,337 @@ func NewDistributedApplicationResourceEventSubscription(handle *Handle, client * } } +// DockerfileBuilder wraps a handle for Aspire.Hosting/Aspire.Hosting.ApplicationModel.Docker.DockerfileBuilder. +type DockerfileBuilder struct { + HandleWrapperBase +} + +// NewDockerfileBuilder creates a new DockerfileBuilder. +func NewDockerfileBuilder(handle *Handle, client *AspireClient) *DockerfileBuilder { + return &DockerfileBuilder{ + HandleWrapperBase: NewHandleWrapperBase(handle, client), + } +} + +// Arg adds a global ARG statement to the Dockerfile +func (s *DockerfileBuilder) Arg(name string, defaultValue *string) (*DockerfileBuilder, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["name"] = SerializeValue(name) + if defaultValue != nil { + reqArgs["defaultValue"] = SerializeValue(defaultValue) + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/dockerfileBuilderArg", reqArgs) + if err != nil { + return nil, err + } + return result.(*DockerfileBuilder), nil +} + +// From adds a FROM statement to start a Dockerfile stage +func (s *DockerfileBuilder) From(image string, stageName *string) (*DockerfileStage, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["image"] = SerializeValue(image) + if stageName != nil { + reqArgs["stageName"] = SerializeValue(stageName) + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/dockerfileBuilderFrom", reqArgs) + if err != nil { + return nil, err + } + return result.(*DockerfileStage), nil +} + +// AddContainerFilesStages adds Dockerfile stages for published container files +func (s *DockerfileBuilder) AddContainerFilesStages(resource *IResource, logger *ILogger) (*DockerfileBuilder, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["resource"] = SerializeValue(resource) + if logger != nil { + reqArgs["logger"] = SerializeValue(logger) + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/dockerfileBuilderAddContainerFilesStages", reqArgs) + if err != nil { + return nil, err + } + return result.(*DockerfileBuilder), nil +} + +// DockerfileBuilderCallbackContext wraps a handle for Aspire.Hosting/Aspire.Hosting.ApplicationModel.DockerfileBuilderCallbackContext. +type DockerfileBuilderCallbackContext struct { + HandleWrapperBase +} + +// NewDockerfileBuilderCallbackContext creates a new DockerfileBuilderCallbackContext. +func NewDockerfileBuilderCallbackContext(handle *Handle, client *AspireClient) *DockerfileBuilderCallbackContext { + return &DockerfileBuilderCallbackContext{ + HandleWrapperBase: NewHandleWrapperBase(handle, client), + } +} + +// Resource gets the Resource property +func (s *DockerfileBuilderCallbackContext) Resource() (*IResource, error) { + reqArgs := map[string]any{ + "context": SerializeValue(s.Handle()), + } + result, err := s.Client().InvokeCapability("Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.resource", reqArgs) + if err != nil { + return nil, err + } + return result.(*IResource), nil +} + +// Builder gets the Builder property +func (s *DockerfileBuilderCallbackContext) Builder() (*DockerfileBuilder, error) { + reqArgs := map[string]any{ + "context": SerializeValue(s.Handle()), + } + result, err := s.Client().InvokeCapability("Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.builder", reqArgs) + if err != nil { + return nil, err + } + return result.(*DockerfileBuilder), nil +} + +// Services gets the Services property +func (s *DockerfileBuilderCallbackContext) Services() (*IServiceProvider, error) { + reqArgs := map[string]any{ + "context": SerializeValue(s.Handle()), + } + result, err := s.Client().InvokeCapability("Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.services", reqArgs) + if err != nil { + return nil, err + } + return result.(*IServiceProvider), nil +} + +// CancellationToken gets the CancellationToken property +func (s *DockerfileBuilderCallbackContext) CancellationToken() (*CancellationToken, error) { + reqArgs := map[string]any{ + "context": SerializeValue(s.Handle()), + } + result, err := s.Client().InvokeCapability("Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.cancellationToken", reqArgs) + if err != nil { + return nil, err + } + return result.(*CancellationToken), nil +} + +// DockerfileStage wraps a handle for Aspire.Hosting/Aspire.Hosting.ApplicationModel.Docker.DockerfileStage. +type DockerfileStage struct { + HandleWrapperBase +} + +// NewDockerfileStage creates a new DockerfileStage. +func NewDockerfileStage(handle *Handle, client *AspireClient) *DockerfileStage { + return &DockerfileStage{ + HandleWrapperBase: NewHandleWrapperBase(handle, client), + } +} + +// Arg adds an ARG statement to a Dockerfile stage +func (s *DockerfileStage) Arg(name string, defaultValue *string) (*DockerfileStage, error) { + reqArgs := map[string]any{ + "stage": SerializeValue(s.Handle()), + } + reqArgs["name"] = SerializeValue(name) + if defaultValue != nil { + reqArgs["defaultValue"] = SerializeValue(defaultValue) + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/dockerfileStageArg", reqArgs) + if err != nil { + return nil, err + } + return result.(*DockerfileStage), nil +} + +// WorkDir adds a WORKDIR statement to a Dockerfile stage +func (s *DockerfileStage) WorkDir(path string) (*DockerfileStage, error) { + reqArgs := map[string]any{ + "stage": SerializeValue(s.Handle()), + } + reqArgs["path"] = SerializeValue(path) + result, err := s.Client().InvokeCapability("Aspire.Hosting/workDir", reqArgs) + if err != nil { + return nil, err + } + return result.(*DockerfileStage), nil +} + +// Run adds a RUN statement to a Dockerfile stage +func (s *DockerfileStage) Run(command string) (*DockerfileStage, error) { + reqArgs := map[string]any{ + "stage": SerializeValue(s.Handle()), + } + reqArgs["command"] = SerializeValue(command) + result, err := s.Client().InvokeCapability("Aspire.Hosting/dockerfileStageRun", reqArgs) + if err != nil { + return nil, err + } + return result.(*DockerfileStage), nil +} + +// Copy adds a COPY statement to a Dockerfile stage +func (s *DockerfileStage) Copy(source string, destination string, chown *string) (*DockerfileStage, error) { + reqArgs := map[string]any{ + "stage": SerializeValue(s.Handle()), + } + reqArgs["source"] = SerializeValue(source) + reqArgs["destination"] = SerializeValue(destination) + if chown != nil { + reqArgs["chown"] = SerializeValue(chown) + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/dockerfileStageCopy", reqArgs) + if err != nil { + return nil, err + } + return result.(*DockerfileStage), nil +} + +// CopyFrom adds a COPY --from statement to a Dockerfile stage +func (s *DockerfileStage) CopyFrom(from string, source string, destination string, chown *string) (*DockerfileStage, error) { + reqArgs := map[string]any{ + "stage": SerializeValue(s.Handle()), + } + reqArgs["from"] = SerializeValue(from) + reqArgs["source"] = SerializeValue(source) + reqArgs["destination"] = SerializeValue(destination) + if chown != nil { + reqArgs["chown"] = SerializeValue(chown) + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/dockerfileStageCopyFrom", reqArgs) + if err != nil { + return nil, err + } + return result.(*DockerfileStage), nil +} + +// Env adds an ENV statement to a Dockerfile stage +func (s *DockerfileStage) Env(name string, value string) (*DockerfileStage, error) { + reqArgs := map[string]any{ + "stage": SerializeValue(s.Handle()), + } + reqArgs["name"] = SerializeValue(name) + reqArgs["value"] = SerializeValue(value) + result, err := s.Client().InvokeCapability("Aspire.Hosting/env", reqArgs) + if err != nil { + return nil, err + } + return result.(*DockerfileStage), nil +} + +// Expose adds an EXPOSE statement to a Dockerfile stage +func (s *DockerfileStage) Expose(port float64) (*DockerfileStage, error) { + reqArgs := map[string]any{ + "stage": SerializeValue(s.Handle()), + } + reqArgs["port"] = SerializeValue(port) + result, err := s.Client().InvokeCapability("Aspire.Hosting/expose", reqArgs) + if err != nil { + return nil, err + } + return result.(*DockerfileStage), nil +} + +// Cmd adds a CMD statement to a Dockerfile stage +func (s *DockerfileStage) Cmd(command []string) (*DockerfileStage, error) { + reqArgs := map[string]any{ + "stage": SerializeValue(s.Handle()), + } + reqArgs["command"] = SerializeValue(command) + result, err := s.Client().InvokeCapability("Aspire.Hosting/cmd", reqArgs) + if err != nil { + return nil, err + } + return result.(*DockerfileStage), nil +} + +// Entrypoint adds an ENTRYPOINT statement to a Dockerfile stage +func (s *DockerfileStage) Entrypoint(command []string) (*DockerfileStage, error) { + reqArgs := map[string]any{ + "stage": SerializeValue(s.Handle()), + } + reqArgs["command"] = SerializeValue(command) + result, err := s.Client().InvokeCapability("Aspire.Hosting/entrypoint", reqArgs) + if err != nil { + return nil, err + } + return result.(*DockerfileStage), nil +} + +// RunWithMounts adds a RUN statement with mounts to a Dockerfile stage +func (s *DockerfileStage) RunWithMounts(command string, mounts []string) (*DockerfileStage, error) { + reqArgs := map[string]any{ + "stage": SerializeValue(s.Handle()), + } + reqArgs["command"] = SerializeValue(command) + reqArgs["mounts"] = SerializeValue(mounts) + result, err := s.Client().InvokeCapability("Aspire.Hosting/runWithMounts", reqArgs) + if err != nil { + return nil, err + } + return result.(*DockerfileStage), nil +} + +// User adds a USER statement to a Dockerfile stage +func (s *DockerfileStage) User(user string) (*DockerfileStage, error) { + reqArgs := map[string]any{ + "stage": SerializeValue(s.Handle()), + } + reqArgs["user"] = SerializeValue(user) + result, err := s.Client().InvokeCapability("Aspire.Hosting/user", reqArgs) + if err != nil { + return nil, err + } + return result.(*DockerfileStage), nil +} + +// EmptyLine adds an empty line to a Dockerfile stage +func (s *DockerfileStage) EmptyLine() (*DockerfileStage, error) { + reqArgs := map[string]any{ + "stage": SerializeValue(s.Handle()), + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/emptyLine", reqArgs) + if err != nil { + return nil, err + } + return result.(*DockerfileStage), nil +} + +// Comment adds a comment to a Dockerfile stage +func (s *DockerfileStage) Comment(comment string) (*DockerfileStage, error) { + reqArgs := map[string]any{ + "stage": SerializeValue(s.Handle()), + } + reqArgs["comment"] = SerializeValue(comment) + result, err := s.Client().InvokeCapability("Aspire.Hosting/comment", reqArgs) + if err != nil { + return nil, err + } + return result.(*DockerfileStage), nil +} + +// AddContainerFiles adds COPY --from statements for published container files +func (s *DockerfileStage) AddContainerFiles(resource *IResource, rootDestinationPath string, logger *ILogger) (*DockerfileStage, error) { + reqArgs := map[string]any{ + "stage": SerializeValue(s.Handle()), + } + reqArgs["resource"] = SerializeValue(resource) + reqArgs["rootDestinationPath"] = SerializeValue(rootDestinationPath) + if logger != nil { + reqArgs["logger"] = SerializeValue(logger) + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/dockerfileStageAddContainerFiles", reqArgs) + if err != nil { + return nil, err + } + return result.(*DockerfileStage), nil +} + // DotnetToolResource wraps a handle for Aspire.Hosting/Aspire.Hosting.ApplicationModel.DotnetToolResource. type DotnetToolResource struct { ResourceBuilderBase @@ -9212,6 +9562,26 @@ func (s *IDistributedApplicationBuilder) AddDockerfile(name string, contextPath return result.(*ContainerResource), nil } +// AddDockerfileBuilder adds a container resource built from a programmatically generated Dockerfile +func (s *IDistributedApplicationBuilder) AddDockerfileBuilder(name string, contextPath string, callback func(...any) any, stage *string) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["name"] = SerializeValue(name) + reqArgs["contextPath"] = SerializeValue(contextPath) + if callback != nil { + reqArgs["callback"] = RegisterCallback(callback) + } + if stage != nil { + reqArgs["stage"] = SerializeValue(stage) + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/addDockerfileBuilder", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + // AddDotnetTool adds a .NET tool resource func (s *IDistributedApplicationBuilder) AddDotnetTool(name string, packageId string) (*DotnetToolResource, error) { reqArgs := map[string]any{ @@ -13743,6 +14113,25 @@ func (s *TestDatabaseResource) WithEndpointProxySupport(proxyEnabled bool) (*Con return result.(*ContainerResource), nil } +// WithDockerfileBuilder configures the resource to use a programmatically generated Dockerfile +func (s *TestDatabaseResource) WithDockerfileBuilder(contextPath string, callback func(...any) any, stage *string) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["contextPath"] = SerializeValue(contextPath) + if callback != nil { + reqArgs["callback"] = RegisterCallback(callback) + } + if stage != nil { + reqArgs["stage"] = SerializeValue(stage) + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/withDockerfileBuilder", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + // WithDockerfileBaseImage sets the base image for a Dockerfile build func (s *TestDatabaseResource) WithDockerfileBaseImage(buildImage *string, runtimeImage *string) (*IResource, error) { reqArgs := map[string]any{ @@ -15362,6 +15751,25 @@ func (s *TestRedisResource) WithEndpointProxySupport(proxyEnabled bool) (*Contai return result.(*ContainerResource), nil } +// WithDockerfileBuilder configures the resource to use a programmatically generated Dockerfile +func (s *TestRedisResource) WithDockerfileBuilder(contextPath string, callback func(...any) any, stage *string) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["contextPath"] = SerializeValue(contextPath) + if callback != nil { + reqArgs["callback"] = RegisterCallback(callback) + } + if stage != nil { + reqArgs["stage"] = SerializeValue(stage) + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/withDockerfileBuilder", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + // WithDockerfileBaseImage sets the base image for a Dockerfile build func (s *TestRedisResource) WithDockerfileBaseImage(buildImage *string, runtimeImage *string) (*IResource, error) { reqArgs := map[string]any{ @@ -17206,6 +17614,25 @@ func (s *TestVaultResource) WithEndpointProxySupport(proxyEnabled bool) (*Contai return result.(*ContainerResource), nil } +// WithDockerfileBuilder configures the resource to use a programmatically generated Dockerfile +func (s *TestVaultResource) WithDockerfileBuilder(contextPath string, callback func(...any) any, stage *string) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["contextPath"] = SerializeValue(contextPath) + if callback != nil { + reqArgs["callback"] = RegisterCallback(callback) + } + if stage != nil { + reqArgs["stage"] = SerializeValue(stage) + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/withDockerfileBuilder", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + // WithDockerfileBaseImage sets the base image for a Dockerfile build func (s *TestVaultResource) WithDockerfileBaseImage(buildImage *string, runtimeImage *string) (*IResource, error) { reqArgs := map[string]any{ @@ -18692,6 +19119,9 @@ func init() { RegisterHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.DistributedApplicationModel", func(h *Handle, c *AspireClient) any { return NewDistributedApplicationModel(h, c) }) + RegisterHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.DockerfileBuilderCallbackContext", func(h *Handle, c *AspireClient) any { + return NewDockerfileBuilderCallbackContext(h, c) + }) RegisterHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.EndpointReferenceExpression", func(h *Handle, c *AspireClient) any { return NewEndpointReferenceExpression(h, c) }) @@ -18725,6 +19155,12 @@ func init() { RegisterHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.ResourceUrlsCallbackContext", func(h *Handle, c *AspireClient) any { return NewResourceUrlsCallbackContext(h, c) }) + RegisterHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.Docker.DockerfileBuilder", func(h *Handle, c *AspireClient) any { + return NewDockerfileBuilder(h, c) + }) + RegisterHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.Docker.DockerfileStage", func(h *Handle, c *AspireClient) any { + return NewDockerfileStage(h, c) + }) RegisterHandleWrapper("Aspire.Hosting/Aspire.Hosting.ConnectionStringResource", func(h *Handle, c *AspireClient) any { return NewConnectionStringResource(h, c) }) diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index 8a31aa3be48..880e13e1733 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -1,4 +1,4 @@ -//! aspire.rs - Capability-based Aspire SDK +//! aspire.rs - Capability-based Aspire SDK //! GENERATED CODE - DO NOT EDIT use std::collections::HashMap; @@ -3347,6 +3347,21 @@ impl ContainerResource { Ok(ContainerResource::new(handle, self.client.clone())) } + /// Configures the resource to use a programmatically generated Dockerfile + pub fn with_dockerfile_builder(&self, context_path: &str, callback: impl Fn(Vec) -> Value + Send + Sync + 'static, stage: Option<&str>) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("contextPath".to_string(), serde_json::to_value(&context_path).unwrap_or(Value::Null)); + let callback_id = register_callback(callback); + args.insert("callback".to_string(), Value::String(callback_id)); + if let Some(ref v) = stage { + args.insert("stage".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/withDockerfileBuilder", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + /// Sets the base image for a Dockerfile build pub fn with_dockerfile_base_image(&self, build_image: Option<&str>, runtime_image: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -4612,6 +4627,316 @@ impl DistributedApplicationResourceEventSubscription { } } +/// Wrapper for Aspire.Hosting/Aspire.Hosting.ApplicationModel.Docker.DockerfileBuilder +pub struct DockerfileBuilder { + handle: Handle, + client: Arc, +} + +impl HasHandle for DockerfileBuilder { + fn handle(&self) -> &Handle { + &self.handle + } +} + +impl DockerfileBuilder { + pub fn new(handle: Handle, client: Arc) -> Self { + Self { handle, client } + } + + pub fn handle(&self) -> &Handle { + &self.handle + } + + pub fn client(&self) -> &Arc { + &self.client + } + + /// Adds a global ARG statement to the Dockerfile + pub fn arg(&self, name: &str, default_value: Option<&str>) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); + if let Some(ref v) = default_value { + args.insert("defaultValue".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/dockerfileBuilderArg", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(DockerfileBuilder::new(handle, self.client.clone())) + } + + /// Adds a FROM statement to start a Dockerfile stage + pub fn from(&self, image: &str, stage_name: Option<&str>) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("image".to_string(), serde_json::to_value(&image).unwrap_or(Value::Null)); + if let Some(ref v) = stage_name { + args.insert("stageName".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/dockerfileBuilderFrom", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(DockerfileStage::new(handle, self.client.clone())) + } + + /// Adds Dockerfile stages for published container files + pub fn add_container_files_stages(&self, resource: &IResource, logger: Option<&ILogger>) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("resource".to_string(), resource.handle().to_json()); + if let Some(ref v) = logger { + args.insert("logger".to_string(), v.handle().to_json()); + } + let result = self.client.invoke_capability("Aspire.Hosting/dockerfileBuilderAddContainerFilesStages", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(DockerfileBuilder::new(handle, self.client.clone())) + } +} + +/// Wrapper for Aspire.Hosting/Aspire.Hosting.ApplicationModel.DockerfileBuilderCallbackContext +pub struct DockerfileBuilderCallbackContext { + handle: Handle, + client: Arc, +} + +impl HasHandle for DockerfileBuilderCallbackContext { + fn handle(&self) -> &Handle { + &self.handle + } +} + +impl DockerfileBuilderCallbackContext { + pub fn new(handle: Handle, client: Arc) -> Self { + Self { handle, client } + } + + pub fn handle(&self) -> &Handle { + &self.handle + } + + pub fn client(&self) -> &Arc { + &self.client + } + + /// Gets the Resource property + pub fn resource(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("context".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.resource", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Gets the Builder property + pub fn builder(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("context".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.builder", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(DockerfileBuilder::new(handle, self.client.clone())) + } + + /// Gets the Services property + pub fn services(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("context".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.services", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IServiceProvider::new(handle, self.client.clone())) + } + + /// Gets the CancellationToken property + pub fn cancellation_token(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("context".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting.ApplicationModel/DockerfileBuilderCallbackContext.cancellationToken", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(CancellationToken::new(handle, self.client.clone())) + } +} + +/// Wrapper for Aspire.Hosting/Aspire.Hosting.ApplicationModel.Docker.DockerfileStage +pub struct DockerfileStage { + handle: Handle, + client: Arc, +} + +impl HasHandle for DockerfileStage { + fn handle(&self) -> &Handle { + &self.handle + } +} + +impl DockerfileStage { + pub fn new(handle: Handle, client: Arc) -> Self { + Self { handle, client } + } + + pub fn handle(&self) -> &Handle { + &self.handle + } + + pub fn client(&self) -> &Arc { + &self.client + } + + /// Adds an ARG statement to a Dockerfile stage + pub fn arg(&self, name: &str, default_value: Option<&str>) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("stage".to_string(), self.handle.to_json()); + args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); + if let Some(ref v) = default_value { + args.insert("defaultValue".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/dockerfileStageArg", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(DockerfileStage::new(handle, self.client.clone())) + } + + /// Adds a WORKDIR statement to a Dockerfile stage + pub fn work_dir(&self, path: &str) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("stage".to_string(), self.handle.to_json()); + args.insert("path".to_string(), serde_json::to_value(&path).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/workDir", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(DockerfileStage::new(handle, self.client.clone())) + } + + /// Adds a RUN statement to a Dockerfile stage + pub fn run(&self, command: &str) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("stage".to_string(), self.handle.to_json()); + args.insert("command".to_string(), serde_json::to_value(&command).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/dockerfileStageRun", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(DockerfileStage::new(handle, self.client.clone())) + } + + /// Adds a COPY statement to a Dockerfile stage + pub fn copy(&self, source: &str, destination: &str, chown: Option<&str>) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("stage".to_string(), self.handle.to_json()); + args.insert("source".to_string(), serde_json::to_value(&source).unwrap_or(Value::Null)); + args.insert("destination".to_string(), serde_json::to_value(&destination).unwrap_or(Value::Null)); + if let Some(ref v) = chown { + args.insert("chown".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/dockerfileStageCopy", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(DockerfileStage::new(handle, self.client.clone())) + } + + /// Adds a COPY --from statement to a Dockerfile stage + pub fn copy_from(&self, from: &str, source: &str, destination: &str, chown: Option<&str>) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("stage".to_string(), self.handle.to_json()); + args.insert("from".to_string(), serde_json::to_value(&from).unwrap_or(Value::Null)); + args.insert("source".to_string(), serde_json::to_value(&source).unwrap_or(Value::Null)); + args.insert("destination".to_string(), serde_json::to_value(&destination).unwrap_or(Value::Null)); + if let Some(ref v) = chown { + args.insert("chown".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/dockerfileStageCopyFrom", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(DockerfileStage::new(handle, self.client.clone())) + } + + /// Adds an ENV statement to a Dockerfile stage + pub fn env(&self, name: &str, value: &str) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("stage".to_string(), self.handle.to_json()); + args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); + args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/env", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(DockerfileStage::new(handle, self.client.clone())) + } + + /// Adds an EXPOSE statement to a Dockerfile stage + pub fn expose(&self, port: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("stage".to_string(), self.handle.to_json()); + args.insert("port".to_string(), serde_json::to_value(&port).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/expose", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(DockerfileStage::new(handle, self.client.clone())) + } + + /// Adds a CMD statement to a Dockerfile stage + pub fn cmd(&self, command: Vec) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("stage".to_string(), self.handle.to_json()); + args.insert("command".to_string(), serde_json::to_value(&command).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/cmd", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(DockerfileStage::new(handle, self.client.clone())) + } + + /// Adds an ENTRYPOINT statement to a Dockerfile stage + pub fn entrypoint(&self, command: Vec) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("stage".to_string(), self.handle.to_json()); + args.insert("command".to_string(), serde_json::to_value(&command).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/entrypoint", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(DockerfileStage::new(handle, self.client.clone())) + } + + /// Adds a RUN statement with mounts to a Dockerfile stage + pub fn run_with_mounts(&self, command: &str, mounts: Vec) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("stage".to_string(), self.handle.to_json()); + args.insert("command".to_string(), serde_json::to_value(&command).unwrap_or(Value::Null)); + args.insert("mounts".to_string(), serde_json::to_value(&mounts).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/runWithMounts", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(DockerfileStage::new(handle, self.client.clone())) + } + + /// Adds a USER statement to a Dockerfile stage + pub fn user(&self, user: &str) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("stage".to_string(), self.handle.to_json()); + args.insert("user".to_string(), serde_json::to_value(&user).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/user", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(DockerfileStage::new(handle, self.client.clone())) + } + + /// Adds an empty line to a Dockerfile stage + pub fn empty_line(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("stage".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/emptyLine", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(DockerfileStage::new(handle, self.client.clone())) + } + + /// Adds a comment to a Dockerfile stage + pub fn comment(&self, comment: &str) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("stage".to_string(), self.handle.to_json()); + args.insert("comment".to_string(), serde_json::to_value(&comment).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/comment", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(DockerfileStage::new(handle, self.client.clone())) + } + + /// Adds COPY --from statements for published container files + pub fn add_container_files(&self, resource: &IResource, root_destination_path: &str, logger: Option<&ILogger>) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("stage".to_string(), self.handle.to_json()); + args.insert("resource".to_string(), resource.handle().to_json()); + args.insert("rootDestinationPath".to_string(), serde_json::to_value(&root_destination_path).unwrap_or(Value::Null)); + if let Some(ref v) = logger { + args.insert("logger".to_string(), v.handle().to_json()); + } + let result = self.client.invoke_capability("Aspire.Hosting/dockerfileStageAddContainerFiles", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(DockerfileStage::new(handle, self.client.clone())) + } +} + /// Wrapper for Aspire.Hosting/Aspire.Hosting.ApplicationModel.DotnetToolResource pub struct DotnetToolResource { handle: Handle, @@ -8013,6 +8338,22 @@ impl IDistributedApplicationBuilder { Ok(ContainerResource::new(handle, self.client.clone())) } + /// Adds a container resource built from a programmatically generated Dockerfile + pub fn add_dockerfile_builder(&self, name: &str, context_path: &str, callback: impl Fn(Vec) -> Value + Send + Sync + 'static, stage: Option<&str>) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); + args.insert("contextPath".to_string(), serde_json::to_value(&context_path).unwrap_or(Value::Null)); + let callback_id = register_callback(callback); + args.insert("callback".to_string(), Value::String(callback_id)); + if let Some(ref v) = stage { + args.insert("stage".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/addDockerfileBuilder", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + /// Adds a .NET tool resource pub fn add_dotnet_tool(&self, name: &str, package_id: &str) -> Result> { let mut args: HashMap = HashMap::new(); @@ -12231,6 +12572,21 @@ impl TestDatabaseResource { Ok(ContainerResource::new(handle, self.client.clone())) } + /// Configures the resource to use a programmatically generated Dockerfile + pub fn with_dockerfile_builder(&self, context_path: &str, callback: impl Fn(Vec) -> Value + Send + Sync + 'static, stage: Option<&str>) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("contextPath".to_string(), serde_json::to_value(&context_path).unwrap_or(Value::Null)); + let callback_id = register_callback(callback); + args.insert("callback".to_string(), Value::String(callback_id)); + if let Some(ref v) = stage { + args.insert("stage".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/withDockerfileBuilder", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + /// Sets the base image for a Dockerfile build pub fn with_dockerfile_base_image(&self, build_image: Option<&str>, runtime_image: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -13537,6 +13893,21 @@ impl TestRedisResource { Ok(ContainerResource::new(handle, self.client.clone())) } + /// Configures the resource to use a programmatically generated Dockerfile + pub fn with_dockerfile_builder(&self, context_path: &str, callback: impl Fn(Vec) -> Value + Send + Sync + 'static, stage: Option<&str>) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("contextPath".to_string(), serde_json::to_value(&context_path).unwrap_or(Value::Null)); + let callback_id = register_callback(callback); + args.insert("callback".to_string(), Value::String(callback_id)); + if let Some(ref v) = stage { + args.insert("stage".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/withDockerfileBuilder", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + /// Sets the base image for a Dockerfile build pub fn with_dockerfile_base_image(&self, build_image: Option<&str>, runtime_image: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -15016,6 +15387,21 @@ impl TestVaultResource { Ok(ContainerResource::new(handle, self.client.clone())) } + /// Configures the resource to use a programmatically generated Dockerfile + pub fn with_dockerfile_builder(&self, context_path: &str, callback: impl Fn(Vec) -> Value + Send + Sync + 'static, stage: Option<&str>) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("contextPath".to_string(), serde_json::to_value(&context_path).unwrap_or(Value::Null)); + let callback_id = register_callback(callback); + args.insert("callback".to_string(), Value::String(callback_id)); + if let Some(ref v) = stage { + args.insert("stage".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/withDockerfileBuilder", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + /// Sets the base image for a Dockerfile build pub fn with_dockerfile_base_image(&self, build_image: Option<&str>, runtime_image: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); From 7d032ffd317f8a283a1c90098b8651d796596a5f Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Fri, 3 Apr 2026 16:09:51 -0700 Subject: [PATCH 3/3] Clarify Dockerfile builder ignore reasons Update the AspireExportIgnore reasons on the synchronous Dockerfile builder overloads to reflect that only the async callback overload is exported to polyglot apphosts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index 74d61936686..96b5939cf13 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -972,7 +972,7 @@ public static IResourceBuilder AddDockerfileBuilder(this IDis /// /// /// This synchronous overload is not available in polyglot app hosts. Use the overload that accepts a . - [AspireExportIgnore(Reason = "DockerfileBuilderCallbackContext is not an ATS-exported callback context.")] + [AspireExportIgnore(Reason = "This synchronous overload is excluded from the polyglot surface; only the async callback overload is exported.")] [Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static IResourceBuilder AddDockerfileBuilder(this IDistributedApplicationBuilder builder, [ResourceName] string name, string contextPath, Action callback, string? stage = null) { @@ -1534,7 +1534,7 @@ public static IResourceBuilder WithDockerfileBuilder(this IResourceBuilder /// /// /// This synchronous overload is not available in polyglot app hosts. Use the overload that accepts a . - [AspireExportIgnore(Reason = "DockerfileBuilderCallbackContext is not an ATS-exported callback context.")] + [AspireExportIgnore(Reason = "This synchronous overload is excluded from the polyglot surface; only the async callback overload is exported.")] [Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static IResourceBuilder WithDockerfileBuilder(this IResourceBuilder builder, string contextPath, Action callback, string? stage = null) where T : ContainerResource {