From 278a9a061ce67f49719fa6f6219b4e891e920807 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:43:39 +0000 Subject: [PATCH 1/6] Initial plan From 53113690d47baa5b970051a6783047db342cd571 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:59:10 +0000 Subject: [PATCH 2/6] Fix withNpm constraint: register concrete types under own key in compatibility map In BuildTypeCompatibilityMap, each concrete type was only registered under its interfaces and base types, but not under its own type ID. This meant that when expanding capabilities constrained to a base type like JavaScriptAppResource, the base type itself was missing from the expanded targets. For example, WithNpm() where TResource : JavaScriptAppResource would only generate withNpm on NodeAppResource and ViteAppResource, but not on JavaScriptAppResource itself. The fix adds self-registration so base types with derived types are always included when expanding capabilities that target them directly. Co-authored-by: IEvangelist <7679720+IEvangelist@users.noreply.github.com> --- .../Ats/AtsCapabilityScanner.cs | 4 + ...TwoPassScanningGeneratedAspire.verified.ts | 497 ++++++++++++++++++ 2 files changed, 501 insertions(+) diff --git a/src/Aspire.Hosting/Ats/AtsCapabilityScanner.cs b/src/Aspire.Hosting/Ats/AtsCapabilityScanner.cs index 8feaa5133b1..92dbe095f27 100644 --- a/src/Aspire.Hosting/Ats/AtsCapabilityScanner.cs +++ b/src/Aspire.Hosting/Ats/AtsCapabilityScanner.cs @@ -786,6 +786,10 @@ private static Dictionary> BuildTypeCompatibilityMap( IsInterface = false }; + // Register under its own type ID so base types with derived types + // are included when expanding capabilities that target them directly + AddToCompatibilityMap(typeToCompatibleTypes, typeInfo.AtsTypeId, concreteTypeRef); + // Register under each implemented interface foreach (var iface in typeInfo.ImplementedInterfaces) { 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 c563cdcd06a..2a783b82785 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -4024,6 +4024,239 @@ export class ContainerResource extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle, source, target }; + if (isReadOnly !== undefined) rpcArgs.isReadOnly = isReadOnly; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withBindMount', + rpcArgs + ); + return new ContainerResource(result, this._client); + } + + /** Adds a bind mount */ + withBindMount(source: string, target: string, options?: WithBindMountOptions): ContainerResourcePromise { + const isReadOnly = options?.isReadOnly; + return new ContainerResourcePromise(this._withBindMountInternal(source, target, isReadOnly)); + } + + /** @internal */ + private async _withEntrypointInternal(entrypoint: string): Promise { + const rpcArgs: Record = { builder: this._handle, entrypoint }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEntrypoint', + rpcArgs + ); + return new ContainerResource(result, this._client); + } + + /** Sets the container entrypoint */ + withEntrypoint(entrypoint: string): ContainerResourcePromise { + return new ContainerResourcePromise(this._withEntrypointInternal(entrypoint)); + } + + /** @internal */ + private async _withImageTagInternal(tag: string): Promise { + const rpcArgs: Record = { builder: this._handle, tag }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withImageTag', + rpcArgs + ); + return new ContainerResource(result, this._client); + } + + /** Sets the container image tag */ + withImageTag(tag: string): ContainerResourcePromise { + return new ContainerResourcePromise(this._withImageTagInternal(tag)); + } + + /** @internal */ + private async _withImageRegistryInternal(registry: string): Promise { + const rpcArgs: Record = { builder: this._handle, registry }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withImageRegistry', + rpcArgs + ); + return new ContainerResource(result, this._client); + } + + /** Sets the container image registry */ + withImageRegistry(registry: string): ContainerResourcePromise { + return new ContainerResourcePromise(this._withImageRegistryInternal(registry)); + } + + /** @internal */ + private async _withImageInternal(image: string, tag?: string): Promise { + const rpcArgs: Record = { builder: this._handle, image }; + if (tag !== undefined) rpcArgs.tag = tag; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withImage', + rpcArgs + ); + return new ContainerResource(result, this._client); + } + + /** Sets the container image */ + withImage(image: string, options?: WithImageOptions): ContainerResourcePromise { + const tag = options?.tag; + return new ContainerResourcePromise(this._withImageInternal(image, tag)); + } + + /** @internal */ + private async _withImageSHA256Internal(sha256: string): Promise { + const rpcArgs: Record = { builder: this._handle, sha256 }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withImageSHA256', + rpcArgs + ); + return new ContainerResource(result, this._client); + } + + /** Sets the image SHA256 digest */ + withImageSHA256(sha256: string): ContainerResourcePromise { + return new ContainerResourcePromise(this._withImageSHA256Internal(sha256)); + } + + /** @internal */ + private async _withContainerRuntimeArgsInternal(args: string[]): Promise { + const rpcArgs: Record = { builder: this._handle, args }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withContainerRuntimeArgs', + rpcArgs + ); + return new ContainerResource(result, this._client); + } + + /** Adds runtime arguments for the container */ + withContainerRuntimeArgs(args: string[]): ContainerResourcePromise { + return new ContainerResourcePromise(this._withContainerRuntimeArgsInternal(args)); + } + + /** @internal */ + private async _withLifetimeInternal(lifetime: ContainerLifetime): Promise { + const rpcArgs: Record = { builder: this._handle, lifetime }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetime', + rpcArgs + ); + return new ContainerResource(result, this._client); + } + + /** Sets the lifetime behavior of the container resource */ + withLifetime(lifetime: ContainerLifetime): ContainerResourcePromise { + return new ContainerResourcePromise(this._withLifetimeInternal(lifetime)); + } + + /** @internal */ + private async _withImagePullPolicyInternal(pullPolicy: ImagePullPolicy): Promise { + const rpcArgs: Record = { builder: this._handle, pullPolicy }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withImagePullPolicy', + rpcArgs + ); + return new ContainerResource(result, this._client); + } + + /** Sets the container image pull policy */ + withImagePullPolicy(pullPolicy: ImagePullPolicy): ContainerResourcePromise { + return new ContainerResourcePromise(this._withImagePullPolicyInternal(pullPolicy)); + } + + /** @internal */ + private async _publishAsContainerInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/publishAsContainer', + rpcArgs + ); + return new ContainerResource(result, this._client); + } + + /** Configures the resource to be published as a container */ + publishAsContainer(): ContainerResourcePromise { + return new ContainerResourcePromise(this._publishAsContainerInternal()); + } + + /** @internal */ + private async _withDockerfileInternal(contextPath: string, dockerfilePath?: string, stage?: string): Promise { + const rpcArgs: Record = { builder: this._handle, contextPath }; + if (dockerfilePath !== undefined) rpcArgs.dockerfilePath = dockerfilePath; + if (stage !== undefined) rpcArgs.stage = stage; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withDockerfile', + rpcArgs + ); + return new ContainerResource(result, this._client); + } + + /** Configures the resource to use a Dockerfile */ + withDockerfile(contextPath: string, options?: WithDockerfileOptions): ContainerResourcePromise { + const dockerfilePath = options?.dockerfilePath; + const stage = options?.stage; + return new ContainerResourcePromise(this._withDockerfileInternal(contextPath, dockerfilePath, stage)); + } + + /** @internal */ + private async _withContainerNameInternal(name: string): Promise { + const rpcArgs: Record = { builder: this._handle, name }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withContainerName', + rpcArgs + ); + return new ContainerResource(result, this._client); + } + + /** Sets the container name */ + withContainerName(name: string): ContainerResourcePromise { + return new ContainerResourcePromise(this._withContainerNameInternal(name)); + } + + /** @internal */ + private async _withBuildArgInternal(name: string, value: ParameterResource): Promise { + const rpcArgs: Record = { builder: this._handle, name, value }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withBuildArg', + rpcArgs + ); + return new ContainerResource(result, this._client); + } + + /** Adds a build argument from a parameter resource */ + withBuildArg(name: string, value: ParameterResource): ContainerResourcePromise { + return new ContainerResourcePromise(this._withBuildArgInternal(name, value)); + } + + /** @internal */ + private async _withBuildSecretInternal(name: string, value: ParameterResource): Promise { + const rpcArgs: Record = { builder: this._handle, name, value }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withBuildSecret', + rpcArgs + ); + return new ContainerResource(result, this._client); + } + + /** Adds a build secret from a parameter resource */ + withBuildSecret(name: string, value: ParameterResource): ContainerResourcePromise { + return new ContainerResourcePromise(this._withBuildSecretInternal(name, value)); + } + + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new ContainerResource(result, this._client); + } + + /** Configures endpoint proxy support */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { + return new ContainerResourcePromise(this._withEndpointProxySupportInternal(proxyEnabled)); + } + /** @internal */ private async _withDockerfileBaseImageInternal(buildImage?: string, runtimeImage?: string): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -4043,6 +4276,21 @@ export class ContainerResource extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle, alias }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withContainerNetworkAlias', + rpcArgs + ); + return new ContainerResource(result, this._client); + } + + /** Adds a network alias for the container */ + withContainerNetworkAlias(alias: string): ContainerResourcePromise { + return new ContainerResourcePromise(this._withContainerNetworkAliasInternal(alias)); + } + /** @internal */ private async _withMcpServerInternal(path?: string, endpointName?: string): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -4092,6 +4340,21 @@ export class ContainerResource extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/publishAsConnectionString', + rpcArgs + ); + return new ContainerResource(result, this._client); + } + + /** Publishes the resource as a connection string */ + publishAsConnectionString(): ContainerResourcePromise { + return new ContainerResourcePromise(this._publishAsConnectionStringInternal()); + } + /** @internal */ private async _withRequiredCommandInternal(command: string, helpLink?: string): Promise { const rpcArgs: Record = { builder: this._handle, command }; @@ -5022,6 +5285,25 @@ export class ContainerResource extends ResourceBuilderBase { + const rpcArgs: Record = { resource: this._handle, target }; + if (name !== undefined) rpcArgs.name = name; + if (isReadOnly !== undefined) rpcArgs.isReadOnly = isReadOnly; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withVolume', + rpcArgs + ); + return new ContainerResource(result, this._client); + } + + /** Adds a volume */ + withVolume(target: string, options?: WithVolumeOptions): ContainerResourcePromise { + const name = options?.name; + const isReadOnly = options?.isReadOnly; + return new ContainerResourcePromise(this._withVolumeInternal(target, name, isReadOnly)); + } + /** Gets the resource name */ async getResourceName(): Promise { const rpcArgs: Record = { resource: this._handle }; @@ -5303,11 +5585,91 @@ export class ContainerResourcePromise implements PromiseLike return new ContainerResourcePromise(this._promise.then(obj => obj.withContainerRegistry(registry))); } + /** Adds a bind mount */ + withBindMount(source: string, target: string, options?: WithBindMountOptions): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.withBindMount(source, target, options))); + } + + /** Sets the container entrypoint */ + withEntrypoint(entrypoint: string): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.withEntrypoint(entrypoint))); + } + + /** Sets the container image tag */ + withImageTag(tag: string): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.withImageTag(tag))); + } + + /** Sets the container image registry */ + withImageRegistry(registry: string): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.withImageRegistry(registry))); + } + + /** Sets the container image */ + withImage(image: string, options?: WithImageOptions): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.withImage(image, options))); + } + + /** Sets the image SHA256 digest */ + withImageSHA256(sha256: string): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.withImageSHA256(sha256))); + } + + /** Adds runtime arguments for the container */ + withContainerRuntimeArgs(args: string[]): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.withContainerRuntimeArgs(args))); + } + + /** Sets the lifetime behavior of the container resource */ + withLifetime(lifetime: ContainerLifetime): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.withLifetime(lifetime))); + } + + /** Sets the container image pull policy */ + withImagePullPolicy(pullPolicy: ImagePullPolicy): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.withImagePullPolicy(pullPolicy))); + } + + /** Configures the resource to be published as a container */ + publishAsContainer(): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.publishAsContainer())); + } + + /** Configures the resource to use a Dockerfile */ + withDockerfile(contextPath: string, options?: WithDockerfileOptions): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.withDockerfile(contextPath, options))); + } + + /** Sets the container name */ + withContainerName(name: string): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.withContainerName(name))); + } + + /** Adds a build argument from a parameter resource */ + withBuildArg(name: string, value: ParameterResource): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.withBuildArg(name, value))); + } + + /** Adds a build secret from a parameter resource */ + withBuildSecret(name: string, value: ParameterResource): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.withBuildSecret(name, value))); + } + + /** Configures endpoint proxy support */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled))); + } + /** Sets the base image for a Dockerfile build */ withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): ContainerResourcePromise { return new ContainerResourcePromise(this._promise.then(obj => obj.withDockerfileBaseImage(options))); } + /** Adds a network alias for the container */ + withContainerNetworkAlias(alias: string): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.withContainerNetworkAlias(alias))); + } + /** Configures an MCP server endpoint on the resource */ withMcpServer(options?: WithMcpServerOptions): ContainerResourcePromise { return new ContainerResourcePromise(this._promise.then(obj => obj.withMcpServer(options))); @@ -5323,6 +5685,11 @@ export class ContainerResourcePromise implements PromiseLike return new ContainerResourcePromise(this._promise.then(obj => obj.withOtlpExporterProtocol(protocol))); } + /** Publishes the resource as a connection string */ + publishAsConnectionString(): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.publishAsConnectionString())); + } + /** Adds a required command dependency */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ContainerResourcePromise { return new ContainerResourcePromise(this._promise.then(obj => obj.withRequiredCommand(command, options))); @@ -5588,6 +5955,11 @@ export class ContainerResourcePromise implements PromiseLike return new ContainerResourcePromise(this._promise.then(obj => obj.withPipelineConfiguration(callback))); } + /** Adds a volume */ + withVolume(target: string, options?: WithVolumeOptions): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.withVolume(target, options))); + } + /** Gets the resource name */ getResourceName(): Promise { return this._promise.then(obj => obj.getResourceName()); @@ -9345,6 +9717,71 @@ export class ExecutableResource extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/publishAsDockerFile', + rpcArgs + ); + return new ExecutableResource(result, this._client); + } + + /** Publishes the executable as a Docker container */ + publishAsDockerFile(): ExecutableResourcePromise { + return new ExecutableResourcePromise(this._publishAsDockerFileInternal()); + } + + /** @internal */ + private async _publishAsDockerFileWithConfigureInternal(configure: (obj: ContainerResource) => Promise): Promise { + const configureId = registerCallback(async (objData: unknown) => { + const objHandle = wrapIfHandle(objData) as ContainerResourceHandle; + const obj = new ContainerResource(objHandle, this._client); + await configure(obj); + }); + const rpcArgs: Record = { builder: this._handle, configure: configureId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/publishAsDockerFileWithConfigure', + rpcArgs + ); + return new ExecutableResource(result, this._client); + } + + /** Publishes an executable as a Docker file with optional container configuration */ + publishAsDockerFileWithConfigure(configure: (obj: ContainerResource) => Promise): ExecutableResourcePromise { + return new ExecutableResourcePromise(this._publishAsDockerFileWithConfigureInternal(configure)); + } + + /** @internal */ + private async _withExecutableCommandInternal(command: string): Promise { + const rpcArgs: Record = { builder: this._handle, command }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withExecutableCommand', + rpcArgs + ); + return new ExecutableResource(result, this._client); + } + + /** Sets the executable command */ + withExecutableCommand(command: string): ExecutableResourcePromise { + return new ExecutableResourcePromise(this._withExecutableCommandInternal(command)); + } + + /** @internal */ + private async _withWorkingDirectoryInternal(workingDirectory: string): Promise { + const rpcArgs: Record = { builder: this._handle, workingDirectory }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withWorkingDirectory', + rpcArgs + ); + return new ExecutableResource(result, this._client); + } + + /** Sets the executable working directory */ + withWorkingDirectory(workingDirectory: string): ExecutableResourcePromise { + return new ExecutableResourcePromise(this._withWorkingDirectoryInternal(workingDirectory)); + } + /** @internal */ private async _withMcpServerInternal(path?: string, endpointName?: string): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -10610,6 +11047,26 @@ export class ExecutableResourcePromise implements PromiseLike obj.withDockerfileBaseImage(options))); } + /** Publishes the executable as a Docker container */ + publishAsDockerFile(): ExecutableResourcePromise { + return new ExecutableResourcePromise(this._promise.then(obj => obj.publishAsDockerFile())); + } + + /** Publishes an executable as a Docker file with optional container configuration */ + publishAsDockerFileWithConfigure(configure: (obj: ContainerResource) => Promise): ExecutableResourcePromise { + return new ExecutableResourcePromise(this._promise.then(obj => obj.publishAsDockerFileWithConfigure(configure))); + } + + /** Sets the executable command */ + withExecutableCommand(command: string): ExecutableResourcePromise { + return new ExecutableResourcePromise(this._promise.then(obj => obj.withExecutableCommand(command))); + } + + /** Sets the executable working directory */ + withWorkingDirectory(workingDirectory: string): ExecutableResourcePromise { + return new ExecutableResourcePromise(this._promise.then(obj => obj.withWorkingDirectory(workingDirectory))); + } + /** Configures an MCP server endpoint on the resource */ withMcpServer(options?: WithMcpServerOptions): ExecutableResourcePromise { return new ExecutableResourcePromise(this._promise.then(obj => obj.withMcpServer(options))); @@ -12626,6 +13083,36 @@ export class ProjectResource extends ResourceBuilderBase return new ProjectResourcePromise(this._withOtlpExporterProtocolInternal(protocol)); } + /** @internal */ + private async _withReplicasInternal(replicas: number): Promise { + const rpcArgs: Record = { builder: this._handle, replicas }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withReplicas', + rpcArgs + ); + return new ProjectResource(result, this._client); + } + + /** Sets the number of replicas */ + withReplicas(replicas: number): ProjectResourcePromise { + return new ProjectResourcePromise(this._withReplicasInternal(replicas)); + } + + /** @internal */ + private async _disableForwardedHeadersInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/disableForwardedHeaders', + rpcArgs + ); + return new ProjectResource(result, this._client); + } + + /** Disables forwarded headers for the project */ + disableForwardedHeaders(): ProjectResourcePromise { + return new ProjectResourcePromise(this._disableForwardedHeadersInternal()); + } + /** @internal */ private async _withRequiredCommandInternal(command: string, helpLink?: string): Promise { const rpcArgs: Record = { builder: this._handle, command }; @@ -13872,6 +14359,16 @@ export class ProjectResourcePromise implements PromiseLike { return new ProjectResourcePromise(this._promise.then(obj => obj.withOtlpExporterProtocol(protocol))); } + /** Sets the number of replicas */ + withReplicas(replicas: number): ProjectResourcePromise { + return new ProjectResourcePromise(this._promise.then(obj => obj.withReplicas(replicas))); + } + + /** Disables forwarded headers for the project */ + disableForwardedHeaders(): ProjectResourcePromise { + return new ProjectResourcePromise(this._promise.then(obj => obj.disableForwardedHeaders())); + } + /** Adds a required command dependency */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ProjectResourcePromise { return new ProjectResourcePromise(this._promise.then(obj => obj.withRequiredCommand(command, options))); From c52777b8d4dbd53257d7342d9e639021a501776f Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 12 Mar 2026 08:07:15 -0500 Subject: [PATCH 3/6] Add tests for JavaScript resource type expansion with npm and package manager methods --- ...ing.CodeGeneration.TypeScript.Tests.csproj | 1 + .../AtsTypeScriptCodeGeneratorTests.cs | 50 +++++++++++++++++++ ...TwoPassScanningGeneratedAspire.verified.ts | 27 ++++++++++ 3 files changed, 78 insertions(+) diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests.csproj b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests.csproj index 8f8f276a0f7..6193224150a 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests.csproj +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs index 015fa7d28f2..2a1dec66ec7 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs @@ -1343,4 +1343,54 @@ private static int CountOccurrences(string text, string pattern) } return count; } + + // ===== JavaScript Assembly Expansion Tests ===== + + [Fact] + public void Scanner_WithNpm_ExpandsToAllJavaScriptResourceTypes() + { + // Verify that withNpm (constrained to JavaScriptAppResource) expands to all three + // concrete JS resource types: JavaScriptAppResource, NodeAppResource, ViteAppResource. + // This is a regression test for capability ID expansion where concrete types + // were not registered under their own type ID in the compatibility map. + var hostingAssembly = typeof(DistributedApplication).Assembly; + var jsAssembly = typeof(Aspire.Hosting.JavaScript.JavaScriptAppResource).Assembly; + + var result = AtsCapabilityScanner.ScanAssemblies([hostingAssembly, jsAssembly]); + + var withNpm = result.Capabilities + .FirstOrDefault(c => c.CapabilityId == "Aspire.Hosting.JavaScript/withNpm"); + Assert.NotNull(withNpm); + + var expandedTypeIds = withNpm.ExpandedTargetTypes.Select(t => t.TypeId).ToList(); + + // All three JS resource types should be present + Assert.Contains(expandedTypeIds, + id => id.Contains("JavaScriptAppResource", StringComparison.Ordinal) + && !id.Contains("NodeApp", StringComparison.Ordinal) + && !id.Contains("ViteApp", StringComparison.Ordinal)); + Assert.Contains(expandedTypeIds, id => id.Contains("NodeAppResource", StringComparison.Ordinal)); + Assert.Contains(expandedTypeIds, id => id.Contains("ViteAppResource", StringComparison.Ordinal)); + } + + [Theory] + [InlineData("withNpm")] + [InlineData("withBun")] + [InlineData("withYarn")] + [InlineData("withPnpm")] + public void Scanner_PackageManagerMethods_ExpandToAllJavaScriptResourceTypes(string methodName) + { + // Verify all package manager methods expand to all three JS resource types + var hostingAssembly = typeof(DistributedApplication).Assembly; + var jsAssembly = typeof(Aspire.Hosting.JavaScript.JavaScriptAppResource).Assembly; + + var result = AtsCapabilityScanner.ScanAssemblies([hostingAssembly, jsAssembly]); + + var capability = result.Capabilities + .FirstOrDefault(c => c.CapabilityId == $"Aspire.Hosting.JavaScript/{methodName}"); + Assert.NotNull(capability); + + var expandedTypeIds = capability.ExpandedTargetTypes.Select(t => t.TypeId).ToList(); + Assert.Equal(3, expandedTypeIds.Count); + } } 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 2a783b82785..7370a025c12 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -13113,6 +13113,28 @@ export class ProjectResource extends ResourceBuilderBase return new ProjectResourcePromise(this._disableForwardedHeadersInternal()); } + /** @internal */ + private async _publishAsDockerFileInternal(configure?: (obj: ContainerResource) => Promise): Promise { + const configureId = configure ? registerCallback(async (objData: unknown) => { + const objHandle = wrapIfHandle(objData) as ContainerResourceHandle; + const obj = new ContainerResource(objHandle, this._client); + await configure(obj); + }) : undefined; + const rpcArgs: Record = { builder: this._handle }; + if (configure !== undefined) rpcArgs.configure = configureId; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/publishProjectAsDockerFileWithConfigure', + rpcArgs + ); + return new ProjectResource(result, this._client); + } + + /** Publishes a project as a Docker file with optional container configuration */ + publishAsDockerFile(options?: PublishAsDockerFileOptions): ProjectResourcePromise { + const configure = options?.configure; + return new ProjectResourcePromise(this._publishAsDockerFileInternal(configure)); + } + /** @internal */ private async _withRequiredCommandInternal(command: string, helpLink?: string): Promise { const rpcArgs: Record = { builder: this._handle, command }; @@ -14369,6 +14391,11 @@ export class ProjectResourcePromise implements PromiseLike { return new ProjectResourcePromise(this._promise.then(obj => obj.disableForwardedHeaders())); } + /** Publishes a project as a Docker file with optional container configuration */ + publishAsDockerFile(options?: PublishAsDockerFileOptions): ProjectResourcePromise { + return new ProjectResourcePromise(this._promise.then(obj => obj.publishAsDockerFile(options))); + } + /** Adds a required command dependency */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ProjectResourcePromise { return new ProjectResourcePromise(this._promise.then(obj => obj.withRequiredCommand(command, options))); From 39805924d32710ebbae8126f2f714267714fd76f Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 12 Mar 2026 09:05:41 -0500 Subject: [PATCH 4/6] Update tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../AtsTypeScriptCodeGeneratorTests.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs index 2a1dec66ec7..f0642ce080d 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs @@ -1365,12 +1365,13 @@ public void Scanner_WithNpm_ExpandsToAllJavaScriptResourceTypes() var expandedTypeIds = withNpm.ExpandedTargetTypes.Select(t => t.TypeId).ToList(); // All three JS resource types should be present - Assert.Contains(expandedTypeIds, - id => id.Contains("JavaScriptAppResource", StringComparison.Ordinal) - && !id.Contains("NodeApp", StringComparison.Ordinal) - && !id.Contains("ViteApp", StringComparison.Ordinal)); - Assert.Contains(expandedTypeIds, id => id.Contains("NodeAppResource", StringComparison.Ordinal)); - Assert.Contains(expandedTypeIds, id => id.Contains("ViteAppResource", StringComparison.Ordinal)); + var javaScriptAppTypeId = AtsTypeMapping.DeriveTypeId(typeof(Aspire.Hosting.JavaScript.JavaScriptAppResource)); + var nodeAppTypeId = AtsTypeMapping.DeriveTypeId(typeof(Aspire.Hosting.JavaScript.NodeAppResource)); + var viteAppTypeId = AtsTypeMapping.DeriveTypeId(typeof(Aspire.Hosting.JavaScript.ViteAppResource)); + + Assert.Contains(javaScriptAppTypeId, expandedTypeIds); + Assert.Contains(nodeAppTypeId, expandedTypeIds); + Assert.Contains(viteAppTypeId, expandedTypeIds); } [Theory] From 8189df4dbd393c5c5d1994f9326172871b80c741 Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 12 Mar 2026 09:05:21 -0500 Subject: [PATCH 5/6] feat: Add new container and project configuration methods - Implemented methods for configuring container resources, including: - `withBindMount`: Adds a bind mount. - `withEntrypoint`: Sets the container entrypoint. - `withImageTag`: Sets the container image tag. - `withImageRegistry`: Sets the container image registry. - `withImage`: Sets the container image with optional tag. - `withImageSHA256`: Sets the image SHA256 digest. - `withContainerRuntimeArgs`: Adds runtime arguments for the container. - `withLifetime`: Sets the lifetime behavior of the container resource. - `withImagePullPolicy`: Sets the container image pull policy. - `publishAsContainer`: Configures the resource to be published as a container. - `withDockerfile`: Configures the resource to use a Dockerfile. - `withContainerName`: Sets the container name. - `withBuildArg`: Adds a build argument from a parameter resource. - `withBuildSecret`: Adds a build secret from a parameter resource. - `withEndpointProxySupport`: Configures endpoint proxy support. - `withContainerNetworkAlias`: Adds a network alias for the container. - `publishAsConnectionString`: Publishes the resource as a connection string. - `withVolume`: Adds a volume. - Added similar methods for executable resources and project resources, including: - `publishAsDockerFile`: Publishes the executable as a Docker container. - `publishAsDockerFileWithConfigure`: Publishes an executable as a Docker file with optional container configuration. - `withExecutableCommand`: Sets the executable command. - `withWorkingDirectory`: Sets the executable working directory. - `withReplicas`: Sets the number of replicas for a project. - `disableForwardedHeaders`: Disables forwarded headers for the project. - `publishAsDockerFile`: Publishes a project as a Docker file with optional container configuration. --- ...TwoPassScanningGeneratedAspire.verified.go | 334 ++++++++++++++++++ ...oPassScanningGeneratedAspire.verified.java | 221 ++++++++++++ ...TwoPassScanningGeneratedAspire.verified.py | 163 +++++++++ ...TwoPassScanningGeneratedAspire.verified.rs | 269 ++++++++++++++ 4 files changed, 987 insertions(+) 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 5205679851e..d1963bf455d 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -2580,6 +2580,207 @@ func (s *ContainerResource) WithContainerRegistry(registry *IResource) (*IResour return result.(*IResource), nil } +// WithBindMount adds a bind mount +func (s *ContainerResource) WithBindMount(source string, target string, isReadOnly bool) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["source"] = SerializeValue(source) + reqArgs["target"] = SerializeValue(target) + reqArgs["isReadOnly"] = SerializeValue(isReadOnly) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBindMount", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + +// WithEntrypoint sets the container entrypoint +func (s *ContainerResource) WithEntrypoint(entrypoint string) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["entrypoint"] = SerializeValue(entrypoint) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withEntrypoint", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + +// WithImageTag sets the container image tag +func (s *ContainerResource) WithImageTag(tag string) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["tag"] = SerializeValue(tag) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withImageTag", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + +// WithImageRegistry sets the container image registry +func (s *ContainerResource) WithImageRegistry(registry string) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["registry"] = SerializeValue(registry) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withImageRegistry", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + +// WithImage sets the container image +func (s *ContainerResource) WithImage(image string, tag string) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["image"] = SerializeValue(image) + reqArgs["tag"] = SerializeValue(tag) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withImage", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + +// WithImageSHA256 sets the image SHA256 digest +func (s *ContainerResource) WithImageSHA256(sha256 string) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["sha256"] = SerializeValue(sha256) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withImageSHA256", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + +// WithContainerRuntimeArgs adds runtime arguments for the container +func (s *ContainerResource) WithContainerRuntimeArgs(args []string) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["args"] = SerializeValue(args) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withContainerRuntimeArgs", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + +// WithLifetime sets the lifetime behavior of the container resource +func (s *ContainerResource) WithLifetime(lifetime ContainerLifetime) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["lifetime"] = SerializeValue(lifetime) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withLifetime", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + +// WithImagePullPolicy sets the container image pull policy +func (s *ContainerResource) WithImagePullPolicy(pullPolicy ImagePullPolicy) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["pullPolicy"] = SerializeValue(pullPolicy) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withImagePullPolicy", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + +// PublishAsContainer configures the resource to be published as a container +func (s *ContainerResource) PublishAsContainer() (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/publishAsContainer", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + +// WithDockerfile configures the resource to use a Dockerfile +func (s *ContainerResource) WithDockerfile(contextPath string, dockerfilePath string, stage string) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["contextPath"] = SerializeValue(contextPath) + reqArgs["dockerfilePath"] = SerializeValue(dockerfilePath) + reqArgs["stage"] = SerializeValue(stage) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withDockerfile", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + +// WithContainerName sets the container name +func (s *ContainerResource) WithContainerName(name string) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["name"] = SerializeValue(name) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withContainerName", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + +// WithBuildArg adds a build argument from a parameter resource +func (s *ContainerResource) WithBuildArg(name string, value *ParameterResource) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["name"] = SerializeValue(name) + reqArgs["value"] = SerializeValue(value) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuildArg", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + +// WithBuildSecret adds a build secret from a parameter resource +func (s *ContainerResource) WithBuildSecret(name string, value *ParameterResource) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["name"] = SerializeValue(name) + reqArgs["value"] = SerializeValue(value) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuildSecret", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + +// WithEndpointProxySupport configures endpoint proxy support +func (s *ContainerResource) WithEndpointProxySupport(proxyEnabled bool) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["proxyEnabled"] = SerializeValue(proxyEnabled) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withEndpointProxySupport", 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{ @@ -2594,6 +2795,19 @@ func (s *ContainerResource) WithDockerfileBaseImage(buildImage string, runtimeIm return result.(*IResource), nil } +// WithContainerNetworkAlias adds a network alias for the container +func (s *ContainerResource) WithContainerNetworkAlias(alias string) (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["alias"] = SerializeValue(alias) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withContainerNetworkAlias", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + // WithMcpServer configures an MCP server endpoint on the resource func (s *ContainerResource) WithMcpServer(path string, endpointName string) (*IResourceWithEndpoints, error) { reqArgs := map[string]any{ @@ -2633,6 +2847,18 @@ func (s *ContainerResource) WithOtlpExporterProtocol(protocol OtlpProtocol) (*IR return result.(*IResourceWithEnvironment), nil } +// PublishAsConnectionString publishes the resource as a connection string +func (s *ContainerResource) PublishAsConnectionString() (*ContainerResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/publishAsConnectionString", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + // WithRequiredCommand adds a required command dependency func (s *ContainerResource) WithRequiredCommand(command string, helpLink string) (*IResource, error) { reqArgs := map[string]any{ @@ -3394,6 +3620,21 @@ func (s *ContainerResource) WithPipelineConfiguration(callback func(...any) any) return result.(*IResource), nil } +// WithVolume adds a volume +func (s *ContainerResource) WithVolume(target string, name string, isReadOnly bool) (*ContainerResource, error) { + reqArgs := map[string]any{ + "resource": SerializeValue(s.Handle()), + } + reqArgs["target"] = SerializeValue(target) + reqArgs["name"] = SerializeValue(name) + reqArgs["isReadOnly"] = SerializeValue(isReadOnly) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withVolume", reqArgs) + if err != nil { + return nil, err + } + return result.(*ContainerResource), nil +} + // GetResourceName gets the resource name func (s *ContainerResource) GetResourceName() (*string, error) { reqArgs := map[string]any{ @@ -5256,6 +5497,59 @@ func (s *ExecutableResource) WithDockerfileBaseImage(buildImage string, runtimeI return result.(*IResource), nil } +// PublishAsDockerFile publishes the executable as a Docker container +func (s *ExecutableResource) PublishAsDockerFile() (*ExecutableResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/publishAsDockerFile", reqArgs) + if err != nil { + return nil, err + } + return result.(*ExecutableResource), nil +} + +// PublishAsDockerFileWithConfigure publishes an executable as a Docker file with optional container configuration +func (s *ExecutableResource) PublishAsDockerFileWithConfigure(configure func(...any) any) (*ExecutableResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + if configure != nil { + reqArgs["configure"] = RegisterCallback(configure) + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/publishAsDockerFileWithConfigure", reqArgs) + if err != nil { + return nil, err + } + return result.(*ExecutableResource), nil +} + +// WithExecutableCommand sets the executable command +func (s *ExecutableResource) WithExecutableCommand(command string) (*ExecutableResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["command"] = SerializeValue(command) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withExecutableCommand", reqArgs) + if err != nil { + return nil, err + } + return result.(*ExecutableResource), nil +} + +// WithWorkingDirectory sets the executable working directory +func (s *ExecutableResource) WithWorkingDirectory(workingDirectory string) (*ExecutableResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["workingDirectory"] = SerializeValue(workingDirectory) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withWorkingDirectory", reqArgs) + if err != nil { + return nil, err + } + return result.(*ExecutableResource), nil +} + // WithMcpServer configures an MCP server endpoint on the resource func (s *ExecutableResource) WithMcpServer(path string, endpointName string) (*IResourceWithEndpoints, error) { reqArgs := map[string]any{ @@ -8251,6 +8545,46 @@ func (s *ProjectResource) WithOtlpExporterProtocol(protocol OtlpProtocol) (*IRes return result.(*IResourceWithEnvironment), nil } +// WithReplicas sets the number of replicas +func (s *ProjectResource) WithReplicas(replicas float64) (*ProjectResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + reqArgs["replicas"] = SerializeValue(replicas) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withReplicas", reqArgs) + if err != nil { + return nil, err + } + return result.(*ProjectResource), nil +} + +// DisableForwardedHeaders disables forwarded headers for the project +func (s *ProjectResource) DisableForwardedHeaders() (*ProjectResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/disableForwardedHeaders", reqArgs) + if err != nil { + return nil, err + } + return result.(*ProjectResource), nil +} + +// PublishAsDockerFile publishes a project as a Docker file with optional container configuration +func (s *ProjectResource) PublishAsDockerFile(configure func(...any) any) (*ProjectResource, error) { + reqArgs := map[string]any{ + "builder": SerializeValue(s.Handle()), + } + if configure != nil { + reqArgs["configure"] = RegisterCallback(configure) + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/publishProjectAsDockerFileWithConfigure", reqArgs) + if err != nil { + return nil, err + } + return result.(*ProjectResource), nil +} + // WithRequiredCommand adds a required command dependency func (s *ProjectResource) WithRequiredCommand(command string, helpLink string) (*IResource, error) { reqArgs := map[string]any{ 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 e9977c1d198..f3e4b6a0274 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -2156,6 +2156,140 @@ public IResource withContainerRegistry(IResource registry) { return (IResource) getClient().invokeCapability("Aspire.Hosting/withContainerRegistry", reqArgs); } + /** Adds a bind mount */ + public ContainerResource withBindMount(String source, String target, Boolean isReadOnly) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("source", AspireClient.serializeValue(source)); + reqArgs.put("target", AspireClient.serializeValue(target)); + if (isReadOnly != null) { + reqArgs.put("isReadOnly", AspireClient.serializeValue(isReadOnly)); + } + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withBindMount", reqArgs); + } + + /** Sets the container entrypoint */ + public ContainerResource withEntrypoint(String entrypoint) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("entrypoint", AspireClient.serializeValue(entrypoint)); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withEntrypoint", reqArgs); + } + + /** Sets the container image tag */ + public ContainerResource withImageTag(String tag) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("tag", AspireClient.serializeValue(tag)); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withImageTag", reqArgs); + } + + /** Sets the container image registry */ + public ContainerResource withImageRegistry(String registry) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("registry", AspireClient.serializeValue(registry)); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withImageRegistry", reqArgs); + } + + /** Sets the container image */ + public ContainerResource withImage(String image, String tag) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("image", AspireClient.serializeValue(image)); + if (tag != null) { + reqArgs.put("tag", AspireClient.serializeValue(tag)); + } + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withImage", reqArgs); + } + + /** Sets the image SHA256 digest */ + public ContainerResource withImageSHA256(String sha256) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sha256", AspireClient.serializeValue(sha256)); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withImageSHA256", reqArgs); + } + + /** Adds runtime arguments for the container */ + public ContainerResource withContainerRuntimeArgs(String[] args) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("args", AspireClient.serializeValue(args)); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withContainerRuntimeArgs", reqArgs); + } + + /** Sets the lifetime behavior of the container resource */ + public ContainerResource withLifetime(ContainerLifetime lifetime) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("lifetime", AspireClient.serializeValue(lifetime)); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withLifetime", reqArgs); + } + + /** Sets the container image pull policy */ + public ContainerResource withImagePullPolicy(ImagePullPolicy pullPolicy) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("pullPolicy", AspireClient.serializeValue(pullPolicy)); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withImagePullPolicy", reqArgs); + } + + /** Configures the resource to be published as a container */ + public ContainerResource publishAsContainer() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/publishAsContainer", reqArgs); + } + + /** Configures the resource to use a Dockerfile */ + public ContainerResource withDockerfile(String contextPath, String dockerfilePath, String stage) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("contextPath", AspireClient.serializeValue(contextPath)); + if (dockerfilePath != null) { + reqArgs.put("dockerfilePath", AspireClient.serializeValue(dockerfilePath)); + } + if (stage != null) { + reqArgs.put("stage", AspireClient.serializeValue(stage)); + } + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withDockerfile", reqArgs); + } + + /** Sets the container name */ + public ContainerResource withContainerName(String name) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withContainerName", reqArgs); + } + + /** Adds a build argument from a parameter resource */ + public ContainerResource withBuildArg(String name, ParameterResource value) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + reqArgs.put("value", AspireClient.serializeValue(value)); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withBuildArg", reqArgs); + } + + /** Adds a build secret from a parameter resource */ + public ContainerResource withBuildSecret(String name, ParameterResource value) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + reqArgs.put("value", AspireClient.serializeValue(value)); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withBuildSecret", reqArgs); + } + + /** Configures endpoint proxy support */ + public ContainerResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + } + /** Sets the base image for a Dockerfile build */ public IResource withDockerfileBaseImage(String buildImage, String runtimeImage) { Map reqArgs = new HashMap<>(); @@ -2169,6 +2303,14 @@ public IResource withDockerfileBaseImage(String buildImage, String runtimeImage) return (IResource) getClient().invokeCapability("Aspire.Hosting/withDockerfileBaseImage", reqArgs); } + /** Adds a network alias for the container */ + public ContainerResource withContainerNetworkAlias(String alias) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("alias", AspireClient.serializeValue(alias)); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withContainerNetworkAlias", reqArgs); + } + /** Configures an MCP server endpoint on the resource */ public IResourceWithEndpoints withMcpServer(String path, String endpointName) { Map reqArgs = new HashMap<>(); @@ -2197,6 +2339,13 @@ public IResourceWithEnvironment withOtlpExporterProtocol(OtlpProtocol protocol) return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withOtlpExporterProtocol", reqArgs); } + /** Publishes the resource as a connection string */ + public ContainerResource publishAsConnectionString() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/publishAsConnectionString", reqArgs); + } + /** Adds a required command dependency */ public IResource withRequiredCommand(String command, String helpLink) { Map reqArgs = new HashMap<>(); @@ -2771,6 +2920,20 @@ public IResource withPipelineConfiguration(Function callback) return (IResource) getClient().invokeCapability("Aspire.Hosting/withPipelineConfiguration", reqArgs); } + /** Adds a volume */ + public ContainerResource withVolume(String target, String name, Boolean isReadOnly) { + Map reqArgs = new HashMap<>(); + reqArgs.put("resource", AspireClient.serializeValue(getHandle())); + reqArgs.put("target", AspireClient.serializeValue(target)); + if (name != null) { + reqArgs.put("name", AspireClient.serializeValue(name)); + } + if (isReadOnly != null) { + reqArgs.put("isReadOnly", AspireClient.serializeValue(isReadOnly)); + } + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withVolume", reqArgs); + } + /** Gets the resource name */ public String getResourceName() { Map reqArgs = new HashMap<>(); @@ -4054,6 +4217,39 @@ public IResource withDockerfileBaseImage(String buildImage, String runtimeImage) return (IResource) getClient().invokeCapability("Aspire.Hosting/withDockerfileBaseImage", reqArgs); } + /** Publishes the executable as a Docker container */ + public ExecutableResource publishAsDockerFile() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + return (ExecutableResource) getClient().invokeCapability("Aspire.Hosting/publishAsDockerFile", reqArgs); + } + + /** Publishes an executable as a Docker file with optional container configuration */ + public ExecutableResource publishAsDockerFileWithConfigure(Function configure) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + if (configure != null) { + reqArgs.put("configure", getClient().registerCallback(configure)); + } + return (ExecutableResource) getClient().invokeCapability("Aspire.Hosting/publishAsDockerFileWithConfigure", reqArgs); + } + + /** Sets the executable command */ + public ExecutableResource withExecutableCommand(String command) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("command", AspireClient.serializeValue(command)); + return (ExecutableResource) getClient().invokeCapability("Aspire.Hosting/withExecutableCommand", reqArgs); + } + + /** Sets the executable working directory */ + public ExecutableResource withWorkingDirectory(String workingDirectory) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("workingDirectory", AspireClient.serializeValue(workingDirectory)); + return (ExecutableResource) getClient().invokeCapability("Aspire.Hosting/withWorkingDirectory", reqArgs); + } + /** Configures an MCP server endpoint on the resource */ public IResourceWithEndpoints withMcpServer(String path, String endpointName) { Map reqArgs = new HashMap<>(); @@ -6154,6 +6350,31 @@ public IResourceWithEnvironment withOtlpExporterProtocol(OtlpProtocol protocol) return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withOtlpExporterProtocol", reqArgs); } + /** Sets the number of replicas */ + public ProjectResource withReplicas(double replicas) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("replicas", AspireClient.serializeValue(replicas)); + return (ProjectResource) getClient().invokeCapability("Aspire.Hosting/withReplicas", reqArgs); + } + + /** Disables forwarded headers for the project */ + public ProjectResource disableForwardedHeaders() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + return (ProjectResource) getClient().invokeCapability("Aspire.Hosting/disableForwardedHeaders", reqArgs); + } + + /** Publishes a project as a Docker file with optional container configuration */ + public ProjectResource publishAsDockerFile(Function configure) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + if (configure != null) { + reqArgs.put("configure", getClient().registerCallback(configure)); + } + return (ProjectResource) getClient().invokeCapability("Aspire.Hosting/publishProjectAsDockerFileWithConfigure", reqArgs); + } + /** Adds a required command dependency */ public IResource withRequiredCommand(String command, String helpLink) { Map reqArgs = new HashMap<>(); 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 e2de69e5b7a..1ec0aae3a77 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -1434,6 +1434,105 @@ def with_container_registry(self, registry: IResource) -> IResource: args["registry"] = serialize_value(registry) return self._client.invoke_capability("Aspire.Hosting/withContainerRegistry", args) + def with_bind_mount(self, source: str, target: str, is_read_only: bool = False) -> ContainerResource: + """Adds a bind mount""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["source"] = serialize_value(source) + args["target"] = serialize_value(target) + args["isReadOnly"] = serialize_value(is_read_only) + return self._client.invoke_capability("Aspire.Hosting/withBindMount", args) + + def with_entrypoint(self, entrypoint: str) -> ContainerResource: + """Sets the container entrypoint""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["entrypoint"] = serialize_value(entrypoint) + return self._client.invoke_capability("Aspire.Hosting/withEntrypoint", args) + + def with_image_tag(self, tag: str) -> ContainerResource: + """Sets the container image tag""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["tag"] = serialize_value(tag) + return self._client.invoke_capability("Aspire.Hosting/withImageTag", args) + + def with_image_registry(self, registry: str) -> ContainerResource: + """Sets the container image registry""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["registry"] = serialize_value(registry) + return self._client.invoke_capability("Aspire.Hosting/withImageRegistry", args) + + def with_image(self, image: str, tag: str | None = None) -> ContainerResource: + """Sets the container image""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["image"] = serialize_value(image) + if tag is not None: + args["tag"] = serialize_value(tag) + return self._client.invoke_capability("Aspire.Hosting/withImage", args) + + def with_image_sha256(self, sha256: str) -> ContainerResource: + """Sets the image SHA256 digest""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["sha256"] = serialize_value(sha256) + return self._client.invoke_capability("Aspire.Hosting/withImageSHA256", args) + + def with_container_runtime_args(self, args: list[str]) -> ContainerResource: + """Adds runtime arguments for the container""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["args"] = serialize_value(args) + return self._client.invoke_capability("Aspire.Hosting/withContainerRuntimeArgs", args) + + def with_lifetime(self, lifetime: ContainerLifetime) -> ContainerResource: + """Sets the lifetime behavior of the container resource""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["lifetime"] = serialize_value(lifetime) + return self._client.invoke_capability("Aspire.Hosting/withLifetime", args) + + def with_image_pull_policy(self, pull_policy: ImagePullPolicy) -> ContainerResource: + """Sets the container image pull policy""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["pullPolicy"] = serialize_value(pull_policy) + return self._client.invoke_capability("Aspire.Hosting/withImagePullPolicy", args) + + def publish_as_container(self) -> ContainerResource: + """Configures the resource to be published as a container""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + return self._client.invoke_capability("Aspire.Hosting/publishAsContainer", args) + + def with_dockerfile(self, context_path: str, dockerfile_path: str | None = None, stage: str | None = None) -> ContainerResource: + """Configures the resource to use a Dockerfile""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["contextPath"] = serialize_value(context_path) + if dockerfile_path is not None: + args["dockerfilePath"] = serialize_value(dockerfile_path) + if stage is not None: + args["stage"] = serialize_value(stage) + return self._client.invoke_capability("Aspire.Hosting/withDockerfile", args) + + def with_container_name(self, name: str) -> ContainerResource: + """Sets the container name""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["name"] = serialize_value(name) + return self._client.invoke_capability("Aspire.Hosting/withContainerName", args) + + def with_build_arg(self, name: str, value: ParameterResource) -> ContainerResource: + """Adds a build argument from a parameter resource""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["name"] = serialize_value(name) + args["value"] = serialize_value(value) + return self._client.invoke_capability("Aspire.Hosting/withBuildArg", args) + + def with_build_secret(self, name: str, value: ParameterResource) -> ContainerResource: + """Adds a build secret from a parameter resource""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["name"] = serialize_value(name) + args["value"] = serialize_value(value) + return self._client.invoke_capability("Aspire.Hosting/withBuildSecret", args) + + def with_endpoint_proxy_support(self, proxy_enabled: bool) -> ContainerResource: + """Configures endpoint proxy support""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["proxyEnabled"] = serialize_value(proxy_enabled) + return self._client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args) + def with_dockerfile_base_image(self, build_image: str | None = None, runtime_image: str | None = None) -> IResource: """Sets the base image for a Dockerfile build""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -1443,6 +1542,12 @@ def with_dockerfile_base_image(self, build_image: str | None = None, runtime_ima args["runtimeImage"] = serialize_value(runtime_image) return self._client.invoke_capability("Aspire.Hosting/withDockerfileBaseImage", args) + def with_container_network_alias(self, alias: str) -> ContainerResource: + """Adds a network alias for the container""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["alias"] = serialize_value(alias) + return self._client.invoke_capability("Aspire.Hosting/withContainerNetworkAlias", args) + def with_mcp_server(self, path: str = "/mcp", endpoint_name: str | None = None) -> IResourceWithEndpoints: """Configures an MCP server endpoint on the resource""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -1462,6 +1567,11 @@ def with_otlp_exporter_protocol(self, protocol: OtlpProtocol) -> IResourceWithEn args["protocol"] = serialize_value(protocol) return self._client.invoke_capability("Aspire.Hosting/withOtlpExporterProtocol", args) + def publish_as_connection_string(self) -> ContainerResource: + """Publishes the resource as a connection string""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + return self._client.invoke_capability("Aspire.Hosting/publishAsConnectionString", args) + def with_required_command(self, command: str, help_link: str | None = None) -> IResource: """Adds a required command dependency""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -1883,6 +1993,15 @@ def with_pipeline_configuration(self, callback: Callable[[PipelineConfigurationC args["callback"] = callback_id return self._client.invoke_capability("Aspire.Hosting/withPipelineConfiguration", args) + def with_volume(self, target: str, name: str | None = None, is_read_only: bool = False) -> ContainerResource: + """Adds a volume""" + args: Dict[str, Any] = { "resource": serialize_value(self._handle) } + args["target"] = serialize_value(target) + if name is not None: + args["name"] = serialize_value(name) + args["isReadOnly"] = serialize_value(is_read_only) + return self._client.invoke_capability("Aspire.Hosting/withVolume", args) + def get_resource_name(self) -> str: """Gets the resource name""" args: Dict[str, Any] = { "resource": serialize_value(self._handle) } @@ -2826,6 +2945,31 @@ def with_dockerfile_base_image(self, build_image: str | None = None, runtime_ima args["runtimeImage"] = serialize_value(runtime_image) return self._client.invoke_capability("Aspire.Hosting/withDockerfileBaseImage", args) + def publish_as_docker_file(self) -> ExecutableResource: + """Publishes the executable as a Docker container""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + return self._client.invoke_capability("Aspire.Hosting/publishAsDockerFile", args) + + def publish_as_docker_file_with_configure(self, configure: Callable[[ContainerResource], None]) -> ExecutableResource: + """Publishes an executable as a Docker file with optional container configuration""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + configure_id = register_callback(configure) if configure is not None else None + if configure_id is not None: + args["configure"] = configure_id + return self._client.invoke_capability("Aspire.Hosting/publishAsDockerFileWithConfigure", args) + + def with_executable_command(self, command: str) -> ExecutableResource: + """Sets the executable command""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["command"] = serialize_value(command) + return self._client.invoke_capability("Aspire.Hosting/withExecutableCommand", args) + + def with_working_directory(self, working_directory: str) -> ExecutableResource: + """Sets the executable working directory""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["workingDirectory"] = serialize_value(working_directory) + return self._client.invoke_capability("Aspire.Hosting/withWorkingDirectory", args) + def with_mcp_server(self, path: str = "/mcp", endpoint_name: str | None = None) -> IResourceWithEndpoints: """Configures an MCP server endpoint on the resource""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -4385,6 +4529,25 @@ def with_otlp_exporter_protocol(self, protocol: OtlpProtocol) -> IResourceWithEn args["protocol"] = serialize_value(protocol) return self._client.invoke_capability("Aspire.Hosting/withOtlpExporterProtocol", args) + def with_replicas(self, replicas: float) -> ProjectResource: + """Sets the number of replicas""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["replicas"] = serialize_value(replicas) + return self._client.invoke_capability("Aspire.Hosting/withReplicas", args) + + def disable_forwarded_headers(self) -> ProjectResource: + """Disables forwarded headers for the project""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + return self._client.invoke_capability("Aspire.Hosting/disableForwardedHeaders", args) + + def publish_as_docker_file(self, configure: Callable[[ContainerResource], None] | None = None) -> ProjectResource: + """Publishes a project as a Docker file with optional container configuration""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + configure_id = register_callback(configure) if configure is not None else None + if configure_id is not None: + args["configure"] = configure_id + return self._client.invoke_capability("Aspire.Hosting/publishProjectAsDockerFileWithConfigure", args) + def with_required_command(self, command: str, help_link: str | None = None) -> IResource: """Adds a required command dependency""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } 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 22cba9c57d9..5ec2aeaf225 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -2555,6 +2555,170 @@ impl ContainerResource { Ok(IResource::new(handle, self.client.clone())) } + /// Adds a bind mount + pub fn with_bind_mount(&self, source: &str, target: &str, is_read_only: Option) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("source".to_string(), serde_json::to_value(&source).unwrap_or(Value::Null)); + args.insert("target".to_string(), serde_json::to_value(&target).unwrap_or(Value::Null)); + if let Some(ref v) = is_read_only { + args.insert("isReadOnly".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/withBindMount", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + + /// Sets the container entrypoint + pub fn with_entrypoint(&self, entrypoint: &str) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("entrypoint".to_string(), serde_json::to_value(&entrypoint).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEntrypoint", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + + /// Sets the container image tag + pub fn with_image_tag(&self, tag: &str) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("tag".to_string(), serde_json::to_value(&tag).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withImageTag", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + + /// Sets the container image registry + pub fn with_image_registry(&self, registry: &str) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("registry".to_string(), serde_json::to_value(®istry).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withImageRegistry", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + + /// Sets the container image + pub fn with_image(&self, image: &str, tag: 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) = tag { + args.insert("tag".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/withImage", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + + /// Sets the image SHA256 digest + pub fn with_image_sha256(&self, sha256: &str) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sha256".to_string(), serde_json::to_value(&sha256).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withImageSHA256", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + + /// Adds runtime arguments for the container + pub fn with_container_runtime_args(&self, args: Vec) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("args".to_string(), serde_json::to_value(&args).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withContainerRuntimeArgs", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + + /// Sets the lifetime behavior of the container resource + pub fn with_lifetime(&self, lifetime: ContainerLifetime) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("lifetime".to_string(), serde_json::to_value(&lifetime).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + + /// Sets the container image pull policy + pub fn with_image_pull_policy(&self, pull_policy: ImagePullPolicy) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("pullPolicy".to_string(), serde_json::to_value(&pull_policy).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withImagePullPolicy", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + + /// Configures the resource to be published as a container + pub fn publish_as_container(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/publishAsContainer", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + + /// Configures the resource to use a Dockerfile + pub fn with_dockerfile(&self, context_path: &str, dockerfile_path: Option<&str>, 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)); + if let Some(ref v) = dockerfile_path { + args.insert("dockerfilePath".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + 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/withDockerfile", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + + /// Sets the container name + pub fn with_container_name(&self, name: &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)); + let result = self.client.invoke_capability("Aspire.Hosting/withContainerName", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + + /// Adds a build argument from a parameter resource + pub fn with_build_arg(&self, name: &str, value: &ParameterResource) -> 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("value".to_string(), value.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withBuildArg", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + + /// Adds a build secret from a parameter resource + pub fn with_build_secret(&self, name: &str, value: &ParameterResource) -> 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("value".to_string(), value.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withBuildSecret", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", 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(); @@ -2570,6 +2734,16 @@ impl ContainerResource { Ok(IResource::new(handle, self.client.clone())) } + /// Adds a network alias for the container + pub fn with_container_network_alias(&self, alias: &str) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("alias".to_string(), serde_json::to_value(&alias).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withContainerNetworkAlias", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + /// Configures an MCP server endpoint on the resource pub fn with_mcp_server(&self, path: Option<&str>, endpoint_name: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -2604,6 +2778,15 @@ impl ContainerResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } + /// Publishes the resource as a connection string + pub fn publish_as_connection_string(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/publishAsConnectionString", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + /// Adds a required command dependency pub fn with_required_command(&self, command: &str, help_link: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -3272,6 +3455,22 @@ impl ContainerResource { Ok(IResource::new(handle, self.client.clone())) } + /// Adds a volume + pub fn with_volume(&self, target: &str, name: Option<&str>, is_read_only: Option) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("resource".to_string(), self.handle.to_json()); + args.insert("target".to_string(), serde_json::to_value(&target).unwrap_or(Value::Null)); + if let Some(ref v) = name { + args.insert("name".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = is_read_only { + args.insert("isReadOnly".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/withVolume", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + /// Gets the resource name pub fn get_resource_name(&self) -> Result> { let mut args: HashMap = HashMap::new(); @@ -4945,6 +5144,46 @@ impl ExecutableResource { Ok(IResource::new(handle, self.client.clone())) } + /// Publishes the executable as a Docker container + pub fn publish_as_docker_file(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/publishAsDockerFile", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ExecutableResource::new(handle, self.client.clone())) + } + + /// Publishes an executable as a Docker file with optional container configuration + pub fn publish_as_docker_file_with_configure(&self, configure: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let callback_id = register_callback(configure); + args.insert("configure".to_string(), Value::String(callback_id)); + let result = self.client.invoke_capability("Aspire.Hosting/publishAsDockerFileWithConfigure", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ExecutableResource::new(handle, self.client.clone())) + } + + /// Sets the executable command + pub fn with_executable_command(&self, command: &str) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".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/withExecutableCommand", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ExecutableResource::new(handle, self.client.clone())) + } + + /// Sets the executable working directory + pub fn with_working_directory(&self, working_directory: &str) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("workingDirectory".to_string(), serde_json::to_value(&working_directory).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withWorkingDirectory", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ExecutableResource::new(handle, self.client.clone())) + } + /// Configures an MCP server endpoint on the resource pub fn with_mcp_server(&self, path: Option<&str>, endpoint_name: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -7832,6 +8071,36 @@ impl ProjectResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } + /// Sets the number of replicas + pub fn with_replicas(&self, replicas: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("replicas".to_string(), serde_json::to_value(&replicas).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withReplicas", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ProjectResource::new(handle, self.client.clone())) + } + + /// Disables forwarded headers for the project + pub fn disable_forwarded_headers(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/disableForwardedHeaders", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ProjectResource::new(handle, self.client.clone())) + } + + /// Publishes a project as a Docker file with optional container configuration + pub fn publish_as_docker_file(&self, configure: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let callback_id = register_callback(configure); + args.insert("configure".to_string(), Value::String(callback_id)); + let result = self.client.invoke_capability("Aspire.Hosting/publishProjectAsDockerFileWithConfigure", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ProjectResource::new(handle, self.client.clone())) + } + /// Adds a required command dependency pub fn with_required_command(&self, command: &str, help_link: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); From 91b8b5934d08f40e3f5b2d784b677bffabb7ae6b Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 12 Mar 2026 09:10:27 -0500 Subject: [PATCH 6/6] test: Update package manager methods test to assert minimum expected JS resource types --- .../AtsTypeScriptCodeGeneratorTests.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs index f0642ce080d..699c8d26459 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs @@ -1381,7 +1381,9 @@ public void Scanner_WithNpm_ExpandsToAllJavaScriptResourceTypes() [InlineData("withPnpm")] public void Scanner_PackageManagerMethods_ExpandToAllJavaScriptResourceTypes(string methodName) { - // Verify all package manager methods expand to all three JS resource types + // Verify all package manager methods expand to the known JS resource types. + // Assert the minimum expected set rather than an exact count so the test + // remains valid when new JavaScriptAppResource-derived types are added. var hostingAssembly = typeof(DistributedApplication).Assembly; var jsAssembly = typeof(Aspire.Hosting.JavaScript.JavaScriptAppResource).Assembly; @@ -1392,6 +1394,12 @@ public void Scanner_PackageManagerMethods_ExpandToAllJavaScriptResourceTypes(str Assert.NotNull(capability); var expandedTypeIds = capability.ExpandedTargetTypes.Select(t => t.TypeId).ToList(); - Assert.Equal(3, expandedTypeIds.Count); + Assert.True(expandedTypeIds.Count >= 3, $"Expected at least 3 expanded types but found {expandedTypeIds.Count}"); + Assert.Contains(expandedTypeIds, + id => id.Contains(nameof(JavaScript.JavaScriptAppResource), StringComparison.Ordinal) + && !id.Contains("NodeApp", StringComparison.Ordinal) + && !id.Contains("ViteApp", StringComparison.Ordinal)); + Assert.Contains(expandedTypeIds, id => id.Contains(nameof(JavaScript.NodeAppResource), StringComparison.Ordinal)); + Assert.Contains(expandedTypeIds, id => id.Contains(nameof(JavaScript.ViteAppResource), StringComparison.Ordinal)); } }