Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ kill <pid>
* Do not use Directory.SetCurrentDirectory in tests as it can cause side effects when tests execute concurrently.
* Prefer using shared test service implementations (e.g., project-level `TestServices/` or `Helpers/` directories, or the cross-project `tests/Shared/` folder) rather than creating private implementation classes within individual test files. Reusing existing test fakes and helpers keeps tests consistent, reduces duplication, and makes maintenance easier. Do not create private test classes when a shared one already exists or can be extended.
* MTP diagnostic args (hang dump, crash dump, exit code handling) are defined in `eng/Testing.props` via `MtpBaseArgs`. Do not hardcode these args in workflow YAML. See [docs/ci/mtp-args-pipeline.md](docs/ci/mtp-args-pipeline.md) for details.
* Use `Verify` (snapshot testing) for generated artifacts (files, serialized output, structured text). Prefer `await Verify(value, "ext")` over `Assert.Contains` / `Assert.DoesNotContain` / `Assert.Equal` on the same value. Run the test once to generate the `.received.` file, review it, then rename it to `.verified.` to accept it.

## Running tests

Expand Down
12 changes: 7 additions & 5 deletions src/Aspire.Hosting.Docker/DockerComposeEnvironmentResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -515,14 +515,16 @@ private async Task PrepareAsync(PipelineStepContext context)
var envVar = entry.Value;
var defaultValue = envVar.DefaultValue;

if (defaultValue is null && envVar.Source is ParameterResource parameter)
// Only resolve from the parameter if no static default is already set;
// a caller that provides an explicit default intends to skip parameter resolution.
if (envVar.Source is ParameterResource parameter)
{
defaultValue = await parameter.GetValueAsync(context.CancellationToken).ConfigureAwait(false);
defaultValue ??= await parameter.GetValueAsync(context.CancellationToken).ConfigureAwait(false);
}

if (envVar.Source is ContainerImageReference cir)
else if (envVar.Source is IValueProvider vp)
{
defaultValue = await ((IValueProvider)cir).GetValueAsync(context.CancellationToken).ConfigureAwait(false);
// IValueProvider sources are always resolved dynamically — a static default is never used.
defaultValue = await vp.GetValueAsync(context.CancellationToken).ConfigureAwait(false);
}

envFile.Add(entry.Key, defaultValue, envVar.Description, onlyIfMissing: false);
Expand Down
89 changes: 87 additions & 2 deletions tests/Aspire.Hosting.Docker.Tests/DockerComposePublisherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#pragma warning disable ASPIREPIPELINES003

using System.Text.RegularExpressions;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Docker.Resources.ComposeNodes;
using Aspire.Hosting.Publishing;
Expand Down Expand Up @@ -746,6 +747,90 @@ public void PrepareStep_OverwritesExistingEnvFileWithCustomEnvironmentName()
Assert.DoesNotContain("OLD_STAGING_KEY", envFileContent);
}

[Fact]
public async Task PrepareStep_ResolvesArbitraryIValueProviderSource()
{
using var tempDir = new TestTempDirectory();

var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, tempDir.Path, step: "prepare-docker-compose");
builder.Services.AddSingleton<IResourceContainerImageManager, MockImageBuilder>();

builder.AddDockerComposeEnvironment("docker-compose");

builder.AddContainer("testapp", "testimage")
.WithEnvironment(context =>
{
context.EnvironmentVariables["MY_VAR"] = new TestConditionProvider("resolved-value");
});

var app = builder.Build();
app.Run();

// The compose file uses the user-specified container env var name; the .env file uses the
// name derived from the provider's ValueExpression. Docker Compose interpolates between them.
var composeContent = await File.ReadAllTextAsync(Path.Combine(tempDir.Path, "docker-compose.yaml"));
var envFileContent = await File.ReadAllTextAsync(Path.Combine(tempDir.Path, ".env.Production"));
await Verify(composeContent, "yaml")
.AppendContentAsFile(envFileContent, "env");
}

[Fact]
public async Task PrepareStep_SkipsParameterResolutionWhenStaticDefaultIsSet()
{
using var tempDir = new TestTempDirectory();

var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, tempDir.Path, step: "prepare-docker-compose");
builder.Services.AddSingleton<IResourceContainerImageManager, MockImageBuilder>();

var environment = builder.AddDockerComposeEnvironment("docker-compose");

var param = builder.AddParameter("myparam", "dynamic-value");

builder.AddContainer("testapp", "testimage")
.WithEnvironment("MY_PARAM", param);

// Set a static default before PrepareAsync runs; parameter resolution should be skipped.
environment.ConfigureEnvFile(vars =>
{
if (vars.TryGetValue("MYPARAM", out var envVar))
{
envVar.DefaultValue = "static-override";
}
});

var app = builder.Build();
app.Run();

var envFileContent = await File.ReadAllTextAsync(Path.Combine(tempDir.Path, ".env.Production"));
await Verify(envFileContent, "env");
}

[Fact]
public async Task PrepareStep_ResolvesContainerImageReferenceViaIValueProvider()
{
using var tempDir = new TestTempDirectory();

var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, tempDir.Path, step: "prepare-docker-compose");
builder.Services.AddSingleton<IResourceContainerImageManager, MockImageBuilder>();

builder.AddDockerComposeEnvironment("docker-compose");

// ProjectResource triggers AsContainerImagePlaceholder, which creates a CapturedEnvironmentVariable
// with Source=ContainerImageReference and DefaultValue="project1:latest".
// PrepareAsync should call GetValueAsync() via the IValueProvider branch; with no registry
// configured in the test, GetValueAsync() returns null and the entry is written empty —
// not "project1:latest". If the IValueProvider branch were skipped, the static default would appear.
builder.AddProject<TestProjectWithLaunchSettings>("project1");

var app = builder.Build();
app.Run();

var envFileContent = await File.ReadAllTextAsync(Path.Combine(tempDir.Path, ".env.Production"));
await Verify(envFileContent, "env")
// The image tag includes the current time (e.g. "aspire-deploy-20260525182406"); scrub it so the snapshot is stable across runs.
.ScrubLinesWithReplace(line => Regex.Replace(line, @"aspire-deploy-\d{14}", "aspire-deploy-TIMESTAMP"));
}

[Fact]
public async Task PublishAsync_BindMounts_ReplacedWithEnvironmentPlaceholders()
{
Expand Down Expand Up @@ -887,8 +972,8 @@ public async Task PublishAsync_ConfigureEnvFile_CanRemoveGeneratedPlaceholder()
.ConfigureEnvFile(envVars =>
{
// Find and remove the auto-generated bind mount placeholder for yarp
var keysToRemove = envVars.Where(kv =>
kv.Value.Resource?.Name == "yarp" &&
var keysToRemove = envVars.Where(kv =>
kv.Value.Resource?.Name == "yarp" &&
kv.Value.Source is ContainerMountAnnotation).Select(kv => kv.Key).ToList();

foreach (var key in keysToRemove)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TEST_CONDITION=resolved-value

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
services:
docker-compose-dashboard:
image: "mcr.microsoft.com/dotnet/nightly/aspire-dashboard:latest"
ports:
- "18888"
expose:
- "18889"
- "18890"
networks:
- "aspire"
restart: "always"
testapp:
image: "testimage:latest"
environment:
MY_VAR: "${TEST_CONDITION}"
networks:
- "aspire"
networks:
aspire:
driver: "bridge"
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Container image name for project1
PROJECT1_IMAGE=project1:aspire-deploy-TIMESTAMP

# Default container port for project1
PROJECT1_PORT=8080

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Parameter myparam
MYPARAM=static-override

Loading