Skip to content
Merged
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
17 changes: 9 additions & 8 deletions src/Aspire.Hosting.Azure/AzureBicepResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,11 @@ public virtual BicepTemplateFile GetBicepTemplateFile(string? directory = null,
throw new InvalidOperationException("Multiple template sources are specified.");
}

var path = TemplateFile;
var isTempFile = false;

if (path is null)
if (TemplateFile is null)
{
isTempFile = directory is null;
var isTempFile = directory is null;

path = TempDirectory is null
var path = TempDirectory is null
? Path.Combine(directory ?? Directory.CreateTempSubdirectory("aspire").FullName, $"{Name.ToLowerInvariant()}.module.bicep")
: Path.Combine(TempDirectory, $"{Name.ToLowerInvariant()}.module.bicep");

Expand All @@ -181,10 +178,14 @@ public virtual BicepTemplateFile GetBicepTemplateFile(string? directory = null,
using var fs = File.OpenWrite(path);
resourceStream.CopyTo(fs);
}

return new(path, isTempFile && deleteTemporaryFileOnDispose);
}

var targetPath = directory is not null ? Path.Combine(directory, path) : path;
return new(targetPath, isTempFile && deleteTemporaryFileOnDispose);
// When TemplateFile is specified, return the original path directly.
// The directory parameter is only for writing temporary files when the template
// is from a string or embedded resource, not for combining with an existing file path.
return new(TemplateFile, deleteFileOnDispose: false);
}

/// <summary>
Expand Down
72 changes: 72 additions & 0 deletions tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,76 @@ public async Task BicepResourceHasPipelineStepAnnotationWithCorrectConfiguration
// Assert - Step depends on CreateProvisioningContext
Assert.Contains(AzureEnvironmentResource.CreateProvisioningContextStepName, step.DependsOnSteps);
}

[Fact]
public void GetBicepTemplateFile_WithTemplateFile_ReturnsOriginalPathWhenDirectoryProvided()
{
// This test verifies the fix for https://github.com/dotnet/aspire/issues/13967
// When a templateFile is specified, GetBicepTemplateFile should return the original path
// and not combine it with the directory parameter.

using var tempDir = new TestTempDirectory();

// Create a test bicep file
var bicepFileName = "test-template.bicep";
var bicepFilePath = Path.Combine(tempDir.Path, bicepFileName);
File.WriteAllText(bicepFilePath, "param location string = resourceGroup().location");

// Create the AzureBicepResource with the templateFile
var resource = new AzureBicepResource("test-resource", templateFile: bicepFilePath);

// Create a different directory to pass to GetBicepTemplateFile
var outputDir = Path.Combine(tempDir.Path, "output");
Directory.CreateDirectory(outputDir);

// Get the bicep template file with a directory parameter
using var templateFile = resource.GetBicepTemplateFile(outputDir);

// The path should be the original template file path, not combined with outputDir
Assert.Equal(bicepFilePath, templateFile.Path);
Assert.True(File.Exists(templateFile.Path), $"The template file should exist at {templateFile.Path}");
}

[Fact]
public void GetBicepTemplateFile_WithTemplateFile_ReturnsOriginalPathWithoutDirectory()
{
using var tempDir = new TestTempDirectory();

// Create a test bicep file
var bicepFileName = "test-template.bicep";
var bicepFilePath = Path.Combine(tempDir.Path, bicepFileName);
File.WriteAllText(bicepFilePath, "param location string = resourceGroup().location");

// Create the AzureBicepResource with the templateFile
var resource = new AzureBicepResource("test-resource", templateFile: bicepFilePath);

// Get the bicep template file without a directory parameter
using var templateFile = resource.GetBicepTemplateFile();

// The path should be the original template file path
Assert.Equal(bicepFilePath, templateFile.Path);
}

[Fact]
public void GetBicepTemplateFile_WithTemplateString_WritesToDirectory()
{
using var tempDir = new TestTempDirectory();

var bicepContent = "param location string = resourceGroup().location";

// Create the AzureBicepResource with a template string (not a file)
var resource = new AzureBicepResource("test-resource", templateString: bicepContent);

// Create a directory to pass to GetBicepTemplateFile
var outputDir = Path.Combine(tempDir.Path, "output");
Directory.CreateDirectory(outputDir);

// Get the bicep template file with a directory parameter
using var templateFile = resource.GetBicepTemplateFile(outputDir);

// The path should be in the output directory
Assert.StartsWith(outputDir, templateFile.Path);
Assert.True(File.Exists(templateFile.Path), $"The template file should exist at {templateFile.Path}");
Assert.Equal(bicepContent, File.ReadAllText(templateFile.Path));
}
}
70 changes: 70 additions & 0 deletions tests/Aspire.Hosting.Azure.Tests/AzureEnvironmentResourceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Aspire.Hosting.Utils;
using Azure.Provisioning;
using Azure.Provisioning.Storage;
using Microsoft.DotNet.RemoteExecutor;

namespace Aspire.Hosting.Azure.Tests;

Expand Down Expand Up @@ -238,6 +239,75 @@ public async Task PublishAsync_WithDockerfileFactory_WritesDockerfileToOutputFol
await Verify(actualContent);
}

[Fact]
public void AzurePublishingContext_WithBicepTemplateFile_WorksWithRelativePath()
{
using var testTempDir = new TestTempDirectory();

var remoteInvokeOptions = new RemoteInvokeOptions();
remoteInvokeOptions.StartInfo.WorkingDirectory = testTempDir.Path;
RemoteExecutor.Invoke(RunTest, testTempDir.Path, remoteInvokeOptions).Dispose();

static async Task RunTest(string tempDir)
{
// This test verifies the fix for https://github.com/dotnet/aspire/issues/13967
// When using AzureBicepResource with a relative templateFile and AzurePublishingContext,
// the bicep file should be correctly copied to the output directory.

// Create a source bicep file (simulating a user's custom bicep template)
var bicepFileName = "custom-resource.bicep";
var bicepFilePath = Path.Combine(tempDir, bicepFileName);
var bicepContent = """
param location string = resourceGroup().location
param customName string

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-02-01' = {
name: customName
location: location
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}

output endpoint string = storageAccount.properties.primaryEndpoints.blob
""";
await File.WriteAllTextAsync(bicepFilePath, bicepContent);

// Create output directory for publishing
var outputDir = Path.Combine(tempDir, "output");
Directory.CreateDirectory(outputDir);

var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: outputDir);

// Add a container app environment (required for publishing)
builder.AddAzureContainerAppEnvironment("env");

// Add the custom AzureBicepResource with a relative template file path
var customResource = new AzureBicepResource("custom-resource", bicepFileName);
builder.AddResource(customResource)
.WithParameter("customName", "mystorageaccount");

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

// Verify the bicep file was copied to the output directory
var mainBicepPath = Path.Combine(outputDir, "main.bicep");
Assert.True(File.Exists(mainBicepPath), "main.bicep should be generated");

var resourceBicepPath = Path.Combine(outputDir, "custom-resource", "custom-resource.bicep");
Assert.True(File.Exists(resourceBicepPath), "custom-resource/custom-resource.bicep should be generated");

// Verify the content of the copied file matches the original
var copiedContent = await File.ReadAllTextAsync(resourceBicepPath);
Assert.Equal(bicepContent, copiedContent);

// Verify the main.bicep references the resource
var mainBicepContent = await File.ReadAllTextAsync(mainBicepPath);
Assert.Contains("module custom_resource 'custom-resource/custom-resource.bicep'", mainBicepContent);
}
}

private sealed class ExternalResourceWithParameters(string name) : Resource(name), IResourceWithParameters
{
public IDictionary<string, object?> Parameters { get; } = new Dictionary<string, object?>();
Expand Down
Loading