Skip to content

Commit eaa877c

Browse files
committed
Fix AzureBicepResource template file handling
- Don't combine paths if TemplateFile is specified - Add test to verify bicep resources with template files can be published Fix #13967
1 parent 3801b06 commit eaa877c

File tree

5 files changed

+135
-90
lines changed

5 files changed

+135
-90
lines changed

src/Aspire.Hosting.Azure/AzureBicepResource.cs

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,6 @@ public class AzureBicepResource : Resource, IAzureResource, IResourceWithParamet
3434
/// <param name="templateResourceName">The name of an embedded resource that represents the bicep file.</param>
3535
public AzureBicepResource(string name, string? templateFile = null, string? templateString = null, string? templateResourceName = null) : base(name)
3636
{
37-
// Convert relative template file paths to absolute paths to ensure they remain valid
38-
// regardless of changes to the current working directory
39-
if (templateFile is not null && !Path.IsPathRooted(templateFile))
40-
{
41-
templateFile = Path.GetFullPath(templateFile);
42-
}
43-
4437
TemplateFile = templateFile;
4538
TemplateString = templateString;
4639
TemplateResourceName = templateResourceName;
@@ -159,14 +152,11 @@ public virtual BicepTemplateFile GetBicepTemplateFile(string? directory = null,
159152
throw new InvalidOperationException("Multiple template sources are specified.");
160153
}
161154

162-
var path = TemplateFile;
163-
var isTempFile = false;
164-
165-
if (path is null)
155+
if (TemplateFile is null)
166156
{
167-
isTempFile = directory is null;
157+
var isTempFile = directory is null;
168158

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

@@ -188,19 +178,14 @@ public virtual BicepTemplateFile GetBicepTemplateFile(string? directory = null,
188178
using var fs = File.OpenWrite(path);
189179
resourceStream.CopyTo(fs);
190180
}
191-
}
192-
else if (directory is not null)
193-
{
194-
// When a template file is provided and a directory is specified,
195-
// copy the file to the directory instead of just combining paths
196-
var fileName = Path.GetFileName(path);
197-
var targetPath = Path.Combine(directory, fileName);
198-
File.Copy(path, targetPath, overwrite: true);
199-
return new(targetPath, isTempFile && deleteTemporaryFileOnDispose);
181+
182+
return new(path, isTempFile && deleteTemporaryFileOnDispose);
200183
}
201184

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

206191
/// <summary>

tests/Aspire.Hosting.Azure.Tests/Aspire.Hosting.Azure.Tests.csproj

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,4 @@
5959
<PackageReference Include="Verify.XunitV3" />
6060
</ItemGroup>
6161

62-
<ItemGroup>
63-
<None Update="TestBicepTemplates\**\*.bicep">
64-
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
65-
</None>
66-
</ItemGroup>
67-
6862
</Project>

tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs

Lines changed: 56 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -248,62 +248,74 @@ public async Task BicepResourceHasPipelineStepAnnotationWithCorrectConfiguration
248248
}
249249

250250
[Fact]
251-
public async Task AzureBicepResourceWithTemplateFile_CanBePublished()
251+
public void GetBicepTemplateFile_WithTemplateFile_ReturnsOriginalPathWhenDirectoryProvided()
252252
{
253-
// Arrange
253+
// This test verifies the fix for https://github.com/dotnet/aspire/issues/13967
254+
// When a templateFile is specified, GetBicepTemplateFile should return the original path
255+
// and not combine it with the directory parameter.
256+
254257
using var tempDir = new TestTempDirectory();
255-
256-
// Create a bicep file (simulating a file in the AppHost project)
258+
259+
// Create a test bicep file
257260
var bicepFileName = "test-template.bicep";
258261
var bicepFilePath = Path.Combine(tempDir.Path, bicepFileName);
259-
260-
var bicepContent = """
261-
param location string = resourceGroup().location
262-
param testParameter string
263-
264-
resource testResource 'Microsoft.Storage/storageAccounts@2021-09-01' = {
265-
name: 'teststorage${uniqueString(resourceGroup().id)}'
266-
location: location
267-
sku: {
268-
name: 'Standard_LRS'
269-
}
270-
kind: 'StorageV2'
271-
tags: {
272-
testParam: testParameter
273-
}
274-
}
262+
File.WriteAllText(bicepFilePath, "param location string = resourceGroup().location");
275263

276-
output storageEndpoint string = testResource.properties.primaryEndpoints.blob
277-
""";
278-
await File.WriteAllTextAsync(bicepFilePath, bicepContent);
279-
264+
// Create the AzureBicepResource with the templateFile
265+
var resource = new AzureBicepResource("test-resource", templateFile: bicepFilePath);
266+
267+
// Create a different directory to pass to GetBicepTemplateFile
280268
var outputDir = Path.Combine(tempDir.Path, "output");
281-
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputDir);
269+
Directory.CreateDirectory(outputDir);
282270

283-
// Add Azure Container App Environment (required for publishing)
284-
builder.AddAzureContainerAppEnvironment("acaEnv");
271+
// Get the bicep template file with a directory parameter
272+
using var templateFile = resource.GetBicepTemplateFile(outputDir);
285273

286-
// Add a bicep resource with a template file (using absolute path for test, but in real scenario this would be relative)
287-
var bicepResource = builder.AddBicepTemplate("myresource", bicepFilePath)
288-
.WithParameter("testParameter", "test-value");
274+
// The path should be the original template file path, not combined with outputDir
275+
Assert.Equal(bicepFilePath, templateFile.Path);
276+
Assert.True(File.Exists(templateFile.Path), $"The template file should exist at {templateFile.Path}");
277+
}
289278

290-
// Act
291-
using var app = builder.Build();
292-
app.Run();
279+
[Fact]
280+
public void GetBicepTemplateFile_WithTemplateFile_ReturnsOriginalPathWithoutDirectory()
281+
{
282+
using var tempDir = new TestTempDirectory();
293283

294-
// Assert - Verify the bicep files were created
295-
var mainBicepPath = Path.Combine(outputDir, "main.bicep");
296-
Assert.True(File.Exists(mainBicepPath), "main.bicep should be generated");
284+
// Create a test bicep file
285+
var bicepFileName = "test-template.bicep";
286+
var bicepFilePath = Path.Combine(tempDir.Path, bicepFileName);
287+
File.WriteAllText(bicepFilePath, "param location string = resourceGroup().location");
288+
289+
// Create the AzureBicepResource with the templateFile
290+
var resource = new AzureBicepResource("test-resource", templateFile: bicepFilePath);
291+
292+
// Get the bicep template file without a directory parameter
293+
using var templateFile = resource.GetBicepTemplateFile();
294+
295+
// The path should be the original template file path
296+
Assert.Equal(bicepFilePath, templateFile.Path);
297+
}
298+
299+
[Fact]
300+
public void GetBicepTemplateFile_WithTemplateString_WritesToDirectory()
301+
{
302+
using var tempDir = new TestTempDirectory();
297303

298-
var resourceBicepPath = Path.Combine(outputDir, "myresource", "myresource.bicep");
299-
Assert.True(File.Exists(resourceBicepPath), "myresource/myresource.bicep should be generated");
304+
var bicepContent = "param location string = resourceGroup().location";
305+
306+
// Create the AzureBicepResource with a template string (not a file)
307+
var resource = new AzureBicepResource("test-resource", templateString: bicepContent);
308+
309+
// Create a directory to pass to GetBicepTemplateFile
310+
var outputDir = Path.Combine(tempDir.Path, "output");
311+
Directory.CreateDirectory(outputDir);
300312

301-
// Verify the content of the copied file matches the original
302-
var copiedContent = await File.ReadAllTextAsync(resourceBicepPath);
303-
Assert.Equal(bicepContent, copiedContent);
313+
// Get the bicep template file with a directory parameter
314+
using var templateFile = resource.GetBicepTemplateFile(outputDir);
304315

305-
// Verify the main.bicep references the resource
306-
var mainBicepContent = await File.ReadAllTextAsync(mainBicepPath);
307-
Assert.Contains("module myresource 'myresource/myresource.bicep'", mainBicepContent);
316+
// The path should be in the output directory
317+
Assert.StartsWith(outputDir, templateFile.Path);
318+
Assert.True(File.Exists(templateFile.Path), $"The template file should exist at {templateFile.Path}");
319+
Assert.Equal(bicepContent, File.ReadAllText(templateFile.Path));
308320
}
309321
}

tests/Aspire.Hosting.Azure.Tests/AzureEnvironmentResourceTests.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Aspire.Hosting.Utils;
88
using Azure.Provisioning;
99
using Azure.Provisioning.Storage;
10+
using Microsoft.DotNet.RemoteExecutor;
1011

1112
namespace Aspire.Hosting.Azure.Tests;
1213

@@ -238,6 +239,75 @@ public async Task PublishAsync_WithDockerfileFactory_WritesDockerfileToOutputFol
238239
await Verify(actualContent);
239240
}
240241

242+
[Fact]
243+
public void AzurePublishingContext_WithBicepTemplateFile_WorksWithRelativePath()
244+
{
245+
using var testTempDir = new TestTempDirectory();
246+
247+
var remoteInvokeOptions = new RemoteInvokeOptions();
248+
remoteInvokeOptions.StartInfo.WorkingDirectory = testTempDir.Path;
249+
RemoteExecutor.Invoke(RunTest, testTempDir.Path, remoteInvokeOptions).Dispose();
250+
251+
static async Task RunTest(string tempDir)
252+
{
253+
// This test verifies the fix for https://github.com/dotnet/aspire/issues/13967
254+
// When using AzureBicepResource with a relative templateFile and AzurePublishingContext,
255+
// the bicep file should be correctly copied to the output directory.
256+
257+
// Create a source bicep file (simulating a user's custom bicep template)
258+
var bicepFileName = "custom-resource.bicep";
259+
var bicepFilePath = Path.Combine(tempDir, bicepFileName);
260+
var bicepContent = """
261+
param location string = resourceGroup().location
262+
param customName string
263+
264+
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-02-01' = {
265+
name: customName
266+
location: location
267+
kind: 'StorageV2'
268+
sku: {
269+
name: 'Standard_LRS'
270+
}
271+
}
272+
273+
output endpoint string = storageAccount.properties.primaryEndpoints.blob
274+
""";
275+
await File.WriteAllTextAsync(bicepFilePath, bicepContent);
276+
277+
// Create output directory for publishing
278+
var outputDir = Path.Combine(tempDir, "output");
279+
Directory.CreateDirectory(outputDir);
280+
281+
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: outputDir);
282+
283+
// Add a container app environment (required for publishing)
284+
builder.AddAzureContainerAppEnvironment("env");
285+
286+
// Add the custom AzureBicepResource with a relative template file path
287+
var customResource = new AzureBicepResource("custom-resource", bicepFileName);
288+
builder.AddResource(customResource)
289+
.WithParameter("customName", "mystorageaccount");
290+
291+
var app = builder.Build();
292+
app.Run();
293+
294+
// Verify the bicep file was copied to the output directory
295+
var mainBicepPath = Path.Combine(outputDir, "main.bicep");
296+
Assert.True(File.Exists(mainBicepPath), "main.bicep should be generated");
297+
298+
var resourceBicepPath = Path.Combine(outputDir, "custom-resource", "custom-resource.bicep");
299+
Assert.True(File.Exists(resourceBicepPath), "custom-resource/custom-resource.bicep should be generated");
300+
301+
// Verify the content of the copied file matches the original
302+
var copiedContent = await File.ReadAllTextAsync(resourceBicepPath);
303+
Assert.Equal(bicepContent, copiedContent);
304+
305+
// Verify the main.bicep references the resource
306+
var mainBicepContent = await File.ReadAllTextAsync(mainBicepPath);
307+
Assert.Contains("module custom_resource 'custom-resource/custom-resource.bicep'", mainBicepContent);
308+
}
309+
}
310+
241311
private sealed class ExternalResourceWithParameters(string name) : Resource(name), IResourceWithParameters
242312
{
243313
public IDictionary<string, object?> Parameters { get; } = new Dictionary<string, object?>();

tests/Aspire.Hosting.Azure.Tests/TestBicepTemplates/test-template.bicep

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)