Skip to content

Commit 6489c6e

Browse files
authored
Fixed AddDockerFile to work with compute customization (#6442) (#6446)
- We were not detecting containers with the build annotation and reading the image name from a parameter. Instead, it was using the runtime image name which is incorrect. - Expose DockerfileBuildAnnotation to make this possible. - Added test and updated the playground with a docker file sample
1 parent ea4aeb3 commit 6489c6e

10 files changed

Lines changed: 267 additions & 16 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Use an official Python runtime as a parent image
2+
FROM python:3.8-slim
3+
4+
# Set the working directory in the container
5+
WORKDIR /app
6+
7+
# Copy the current directory contents into the container at /app
8+
COPY . /app
9+
10+
# Run the command to execute app.py when the container starts
11+
CMD ["python", "app.py"]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import time
2+
from datetime import datetime
3+
4+
while True:
5+
print(datetime.now(), flush=True)
6+
time.sleep(3)

playground/AzureContainerApps/AzureContainerApps.AppHost/AzureContainerApps.AppHost.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,8 @@
2424
<ProjectReference Include="..\AzureContainerApps.ApiService\AzureContainerApps.ApiService.csproj" />
2525
</ItemGroup>
2626

27+
<ItemGroup>
28+
<Folder Include="AppWithDocker\" />
29+
</ItemGroup>
30+
2731
</Project>

playground/AzureContainerApps/AzureContainerApps.AppHost/Program.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
.RunAsEmulator(c => c.WithLifetime(ContainerLifetime.Persistent))
2121
.AddBlobs("blobs");
2222

23+
// Testing docker files
24+
25+
builder.AddDockerfile("pythonapp", "AppWithDocker");
26+
27+
// Testing projects
2328
builder.AddProject<Projects.AzureContainerApps_ApiService>("api")
2429
.WithExternalHttpEndpoints()
2530
.WithReference(blobs)

playground/AzureContainerApps/AzureContainerApps.AppHost/aspire-manifest.json

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,24 @@
6666
"type": "value.v0",
6767
"connectionString": "{storage.outputs.blobEndpoint}"
6868
},
69+
"pythonapp": {
70+
"type": "container.v1",
71+
"build": {
72+
"context": "AppWithDocker",
73+
"dockerfile": "AppWithDocker/Dockerfile"
74+
},
75+
"deployment": {
76+
"type": "azure.bicep.v0",
77+
"path": "pythonapp.module.bicep",
78+
"params": {
79+
"outputs_azure_container_registry_managed_identity_id": "{.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}",
80+
"outputs_managed_identity_client_id": "{.outputs.MANAGED_IDENTITY_CLIENT_ID}",
81+
"outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}",
82+
"outputs_azure_container_registry_endpoint": "{.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}",
83+
"pythonapp_containerimage": "{pythonapp.containerImage}"
84+
}
85+
}
86+
},
6987
"api": {
7088
"type": "project.v1",
7189
"path": "../AzureContainerApps.ApiService/AzureContainerApps.ApiService.csproj",
@@ -81,7 +99,9 @@
8199
"outputs_managed_identity_client_id": "{.outputs.MANAGED_IDENTITY_CLIENT_ID}",
82100
"outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}",
83101
"outputs_azure_container_registry_endpoint": "{.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}",
84-
"api_containerimage": "{api.containerImage}"
102+
"api_containerimage": "{api.containerImage}",
103+
"certificateName": "{certificateName.value}",
104+
"customDomain": "{customDomain.value}"
85105
}
86106
},
87107
"env": {
@@ -111,4 +131,4 @@
111131
}
112132
}
113133
}
114-
}
134+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
@description('The location for the resource(s) to be deployed.')
2+
param location string = resourceGroup().location
3+
4+
param outputs_azure_container_registry_managed_identity_id string
5+
6+
param outputs_managed_identity_client_id string
7+
8+
param outputs_azure_container_apps_environment_id string
9+
10+
param outputs_azure_container_registry_endpoint string
11+
12+
param pythonapp_containerimage string
13+
14+
resource pythonapp 'Microsoft.App/containerApps@2024-03-01' = {
15+
name: 'pythonapp'
16+
location: location
17+
properties: {
18+
configuration: {
19+
activeRevisionsMode: 'Single'
20+
registries: [
21+
{
22+
server: outputs_azure_container_registry_endpoint
23+
identity: outputs_azure_container_registry_managed_identity_id
24+
}
25+
]
26+
}
27+
environmentId: outputs_azure_container_apps_environment_id
28+
template: {
29+
containers: [
30+
{
31+
image: pythonapp_containerimage
32+
name: 'pythonapp'
33+
env: [
34+
{
35+
name: 'AZURE_CLIENT_ID'
36+
value: outputs_managed_identity_client_id
37+
}
38+
]
39+
}
40+
]
41+
scale: {
42+
minReplicas: 1
43+
}
44+
}
45+
}
46+
identity: {
47+
type: 'UserAssigned'
48+
userAssignedIdentities: {
49+
'${outputs_azure_container_registry_managed_identity_id}': { }
50+
}
51+
}
52+
}

src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public void BuildContainerApp(AzureResourceInfrastructure c)
136136

137137
ProvisioningParameter? containerImageParam = null;
138138

139-
if (!resource.TryGetContainerImageName(out var containerImageName))
139+
if (!TryGetContainerImageName(resource, out var containerImageName))
140140
{
141141
AllocateContainerRegistryParameters();
142142

@@ -224,6 +224,19 @@ public void BuildContainerApp(AzureResourceInfrastructure c)
224224
}
225225
}
226226

227+
private static bool TryGetContainerImageName(IResource resource, out string? containerImageName)
228+
{
229+
// If the resource has a Dockerfile build annotation, we don't have the image name
230+
// it will come as a parameter
231+
if (resource.TryGetLastAnnotation<DockerfileBuildAnnotation>(out _))
232+
{
233+
containerImageName = null;
234+
return false;
235+
}
236+
237+
return resource.TryGetContainerImageName(out containerImageName);
238+
}
239+
227240
public async Task ProcessResourceAsync(DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken)
228241
{
229242
ProcessEndpoints();
@@ -726,10 +739,10 @@ private BicepValue<string> AllocateKeyVaultSecretUriReference(BicepSecretOutputR
726739
}
727740

728741
private ProvisioningParameter AllocateContainerImageParameter()
729-
=> AllocateParameter(ProjectResourceExpression.GetContainerImageExpression((ProjectResource)resource));
742+
=> AllocateParameter(ResourceExpression.GetContainerImageExpression(resource));
730743

731744
private BicepValue<int> AllocateContainerPortParameter()
732-
=> AllocateParameter(ProjectResourceExpression.GetContainerPortExpression((ProjectResource)resource));
745+
=> AllocateParameter(ResourceExpression.GetContainerPortExpression(resource));
733746

734747
private ProvisioningParameter AllocateManagedIdentityIdParameter()
735748
=> _managedIdentityIdParameter ??= AllocateParameter(_containerAppEnvironmentContext.ManagedIdentityId);
@@ -986,15 +999,15 @@ public static IManifestExpressionProvider GetSecretOutputKeyVault(AzureBicepReso
986999
new SecretOutputExpression(resource);
9871000
}
9881001

989-
private sealed class ProjectResourceExpression(ProjectResource projectResource, string propertyExpression) : IManifestExpressionProvider
1002+
private sealed class ResourceExpression(IResource resource, string propertyExpression) : IManifestExpressionProvider
9901003
{
991-
public string ValueExpression => $"{{{projectResource.Name}.{propertyExpression}}}";
1004+
public string ValueExpression => $"{{{resource.Name}.{propertyExpression}}}";
9921005

993-
public static IManifestExpressionProvider GetContainerImageExpression(ProjectResource p) =>
994-
new ProjectResourceExpression(p, "containerImage");
1006+
public static IManifestExpressionProvider GetContainerImageExpression(IResource p) =>
1007+
new ResourceExpression(p, "containerImage");
9951008

996-
public static IManifestExpressionProvider GetContainerPortExpression(ProjectResource p) =>
997-
new ProjectResourceExpression(p, "containerPort");
1009+
public static IManifestExpressionProvider GetContainerPortExpression(IResource p) =>
1010+
new ResourceExpression(p, "containerPort");
9981011
}
9991012

10001013
/// <summary>

src/Aspire.Hosting/ApplicationModel/DockerfileBuildAnnotation.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,36 @@
33

44
namespace Aspire.Hosting.ApplicationModel;
55

6-
internal class DockerfileBuildAnnotation(string contextPath, string dockerfilePath, string? stage) : IResourceAnnotation
6+
/// <summary>
7+
/// Represents an annotation for customizing a Dockerfile build.
8+
/// </summary>
9+
/// <param name="contextPath">The path to the context directory for the build. </param>
10+
/// <param name="dockerfilePath">The path to the Dockerfile to use for the build.</param>
11+
/// <param name="stage">The name of the build stage to use for the build.</param>
12+
public class DockerfileBuildAnnotation(string contextPath, string dockerfilePath, string? stage) : IResourceAnnotation
713
{
14+
/// <summary>
15+
/// Gets the path to the context directory for the build.
16+
/// </summary>
817
public string ContextPath => contextPath;
9-
public string DockerfilePath = dockerfilePath;
18+
19+
/// <summary>
20+
/// Gets the path to the Dockerfile to use for the build.
21+
/// </summary>
22+
public string DockerfilePath => dockerfilePath;
23+
24+
/// <summary>
25+
/// Gets the name of the build stage to use for the build.
26+
/// </summary>
1027
public string? Stage => stage;
11-
public Dictionary<string, object> BuildArguments { get; } = new();
12-
public Dictionary<string, object> BuildSecrets { get; } = new();
28+
29+
/// <summary>
30+
/// Gets the arguments to pass to the build.
31+
/// </summary>
32+
public Dictionary<string, object> BuildArguments { get; } = [];
33+
34+
/// <summary>
35+
/// Gets the secrets to pass to the build.
36+
/// </summary>
37+
public Dictionary<string, object> BuildSecrets { get; } = [];
1338
}

src/Aspire.Hosting/PublicAPI.Unshipped.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
#nullable enable
22
Aspire.Hosting.ApplicationModel.ContainerLifetime.Session = 0 -> Aspire.Hosting.ApplicationModel.ContainerLifetime
3+
Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.HealthStatus.get -> Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus?
4+
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation
5+
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.BuildArguments.get -> System.Collections.Generic.Dictionary<string!, object!>!
6+
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.BuildSecrets.get -> System.Collections.Generic.Dictionary<string!, object!>!
7+
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.ContextPath.get -> string!
8+
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.DockerfileBuildAnnotation(string! contextPath, string! dockerfilePath, string? stage) -> void
9+
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.DockerfilePath.get -> string!
10+
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.Stage.get -> string?
311
Aspire.Hosting.ApplicationModel.EndpointNameAttribute
412
Aspire.Hosting.ApplicationModel.EndpointNameAttribute.EndpointNameAttribute() -> void
513
Aspire.Hosting.ApplicationModel.HealthReportSnapshot
@@ -46,7 +54,6 @@ Aspire.Hosting.ApplicationModel.ContainerNameAnnotation.Name.get -> string!
4654
Aspire.Hosting.ApplicationModel.ContainerNameAnnotation.Name.set -> void
4755
Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.Commands.get -> System.Collections.Immutable.ImmutableArray<Aspire.Hosting.ApplicationModel.ResourceCommandSnapshot!>
4856
Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.Commands.init -> void
49-
Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.HealthStatus.get -> Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus?
5057
Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.HealthStatus.init -> void
5158
Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.HealthReports.get -> System.Collections.Immutable.ImmutableArray<Aspire.Hosting.ApplicationModel.HealthReportSnapshot!>
5259
Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.HealthReports.init -> void

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

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,114 @@ param outputs_azure_container_apps_environment_id string
104104
Assert.Equal(expectedBicep, bicep);
105105
}
106106

107+
[Fact]
108+
public async Task AddDockerfileWithAppsInfrastructureAddsDeploymentTargetWithContainerAppToContainerResources()
109+
{
110+
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
111+
112+
builder.AddAzureContainerAppsInfrastructure();
113+
114+
var directory = Directory.CreateTempSubdirectory(".aspire-test");
115+
116+
// Contents of the Dockerfile are not important for this test
117+
File.WriteAllText(Path.Combine(directory.FullName, "Dockerfile"), "");
118+
119+
builder.AddDockerfile("api", directory.FullName);
120+
121+
using var app = builder.Build();
122+
123+
await ExecuteBeforeStartHooksAsync(app, default);
124+
125+
var model = app.Services.GetRequiredService<DistributedApplicationModel>();
126+
127+
var container = Assert.Single(model.GetContainerResources());
128+
129+
container.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var target);
130+
131+
var resource = target?.DeploymentTarget as AzureProvisioningResource;
132+
133+
Assert.NotNull(resource);
134+
135+
var (manifest, bicep) = await ManifestUtils.GetManifestWithBicep(resource);
136+
137+
var m = manifest.ToString();
138+
139+
var expectedManifest =
140+
"""
141+
{
142+
"type": "azure.bicep.v0",
143+
"path": "api.module.bicep",
144+
"params": {
145+
"outputs_azure_container_registry_managed_identity_id": "{.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}",
146+
"outputs_managed_identity_client_id": "{.outputs.MANAGED_IDENTITY_CLIENT_ID}",
147+
"outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}",
148+
"outputs_azure_container_registry_endpoint": "{.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}",
149+
"api_containerimage": "{api.containerImage}"
150+
}
151+
}
152+
""";
153+
154+
Assert.Equal(expectedManifest, m);
155+
156+
var expectedBicep =
157+
"""
158+
@description('The location for the resource(s) to be deployed.')
159+
param location string = resourceGroup().location
160+
161+
param outputs_azure_container_registry_managed_identity_id string
162+
163+
param outputs_managed_identity_client_id string
164+
165+
param outputs_azure_container_apps_environment_id string
166+
167+
param outputs_azure_container_registry_endpoint string
168+
169+
param api_containerimage string
170+
171+
resource api 'Microsoft.App/containerApps@2024-03-01' = {
172+
name: 'api'
173+
location: location
174+
properties: {
175+
configuration: {
176+
activeRevisionsMode: 'Single'
177+
registries: [
178+
{
179+
server: outputs_azure_container_registry_endpoint
180+
identity: outputs_azure_container_registry_managed_identity_id
181+
}
182+
]
183+
}
184+
environmentId: outputs_azure_container_apps_environment_id
185+
template: {
186+
containers: [
187+
{
188+
image: api_containerimage
189+
name: 'api'
190+
env: [
191+
{
192+
name: 'AZURE_CLIENT_ID'
193+
value: outputs_managed_identity_client_id
194+
}
195+
]
196+
}
197+
]
198+
scale: {
199+
minReplicas: 1
200+
}
201+
}
202+
}
203+
identity: {
204+
type: 'UserAssigned'
205+
userAssignedIdentities: {
206+
'${outputs_azure_container_registry_managed_identity_id}': { }
207+
}
208+
}
209+
}
210+
""";
211+
output.WriteLine(bicep);
212+
Assert.Equal(expectedBicep, bicep);
213+
}
214+
107215
[Fact]
108216
public async Task AddContainerAppsInfrastructureAddsDeploymentTargetWithContainerAppToProjectResources()
109217
{

0 commit comments

Comments
 (0)