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
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Use an official Python runtime as a parent image
FROM python:3.8-slim

# Set the working directory in the container
WORKDIR /app

# Copy the current directory contents into the container at /app
COPY . /app

# Run the command to execute app.py when the container starts
CMD ["python", "app.py"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import time
from datetime import datetime

while True:
print(datetime.now(), flush=True)
time.sleep(3)
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@
<ProjectReference Include="..\AzureContainerApps.ApiService\AzureContainerApps.ApiService.csproj" />
</ItemGroup>

<ItemGroup>
<Folder Include="AppWithDocker\" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
.RunAsEmulator(c => c.WithLifetime(ContainerLifetime.Persistent))
.AddBlobs("blobs");

// Testing docker files

builder.AddDockerfile("pythonapp", "AppWithDocker");

// Testing projects
builder.AddProject<Projects.AzureContainerApps_ApiService>("api")
.WithExternalHttpEndpoints()
.WithReference(blobs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
],
"volumes": [
{
"name": "azurecontainerapps.apphost-43a728061e-cache-data",
"name": "azurecontainerapps.apphost-a01ec9bc8d-cache-data",
"target": "/data",
"readOnly": false
}
Expand Down Expand Up @@ -84,16 +84,30 @@
"type": "value.v0",
"connectionString": "{storage.outputs.blobEndpoint}"
},
"pythonapp": {
"type": "container.v1",
"build": {
"context": "AppWithDocker",
"dockerfile": "AppWithDocker/Dockerfile"
},
"deployment": {
"type": "azure.bicep.v0",
"path": "pythonapp.module.bicep",
"params": {
"outputs_azure_container_registry_managed_identity_id": "{.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}",
"outputs_managed_identity_client_id": "{.outputs.MANAGED_IDENTITY_CLIENT_ID}",
"outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}",
"outputs_azure_container_registry_endpoint": "{.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}",
"pythonapp_containerimage": "{pythonapp.containerImage}"
}
}
},
"api": {
"type": "project.v1",
"path": "../AzureContainerApps.ApiService/AzureContainerApps.ApiService.csproj",
"deployment": {
"type": "azure.bicep.v0",
"path": "api.module.bicep",
"params": {
"certificateName": "{certificateName.value}",
"customDomain": "{customDomain.value}"
},
"params": {
"api_containerport": "{api.containerPort}",
"storage_outputs_blobendpoint": "{storage.outputs.blobEndpoint}",
Expand All @@ -103,7 +117,9 @@
"outputs_managed_identity_client_id": "{.outputs.MANAGED_IDENTITY_CLIENT_ID}",
"outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}",
"outputs_azure_container_registry_endpoint": "{.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}",
"api_containerimage": "{api.containerImage}"
"api_containerimage": "{api.containerImage}",
"certificateName": "{certificateName.value}",
"customDomain": "{customDomain.value}"
}
},
"env": {
Expand Down Expand Up @@ -133,4 +149,4 @@
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location

param outputs_azure_container_registry_managed_identity_id string

param outputs_managed_identity_client_id string

param outputs_azure_container_apps_environment_id string

param outputs_azure_container_registry_endpoint string

param pythonapp_containerimage string

resource pythonapp 'Microsoft.App/containerApps@2024-03-01' = {
name: 'pythonapp'
location: location
properties: {
configuration: {
activeRevisionsMode: 'Single'
registries: [
{
server: outputs_azure_container_registry_endpoint
identity: outputs_azure_container_registry_managed_identity_id
}
]
}
environmentId: outputs_azure_container_apps_environment_id
template: {
containers: [
{
image: pythonapp_containerimage
name: 'pythonapp'
env: [
{
name: 'AZURE_CLIENT_ID'
value: outputs_managed_identity_client_id
}
]
}
]
scale: {
minReplicas: 1
}
}
}
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${outputs_azure_container_registry_managed_identity_id}': { }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public void BuildContainerApp(AzureResourceInfrastructure c)

ProvisioningParameter? containerImageParam = null;

if (!resource.TryGetContainerImageName(out var containerImageName))
if (!TryGetContainerImageName(resource, out var containerImageName))
{
AllocateContainerRegistryParameters();

Expand Down Expand Up @@ -224,6 +224,19 @@ public void BuildContainerApp(AzureResourceInfrastructure c)
}
}

private static bool TryGetContainerImageName(IResource resource, out string? containerImageName)
{
// If the resource has a Dockerfile build annotation, we don't have the image name
// it will come as a parameter
if (resource.TryGetLastAnnotation<DockerfileBuildAnnotation>(out _))
{
containerImageName = null;
return false;
}

return resource.TryGetContainerImageName(out containerImageName);
}

public async Task ProcessResourceAsync(DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken)
{
ProcessEndpoints();
Expand Down Expand Up @@ -726,10 +739,10 @@ private BicepValue<string> AllocateKeyVaultSecretUriReference(BicepSecretOutputR
}

private ProvisioningParameter AllocateContainerImageParameter()
=> AllocateParameter(ProjectResourceExpression.GetContainerImageExpression((ProjectResource)resource));
=> AllocateParameter(ResourceExpression.GetContainerImageExpression(resource));

private BicepValue<int> AllocateContainerPortParameter()
=> AllocateParameter(ProjectResourceExpression.GetContainerPortExpression((ProjectResource)resource));
=> AllocateParameter(ResourceExpression.GetContainerPortExpression(resource));

private ProvisioningParameter AllocateManagedIdentityIdParameter()
=> _managedIdentityIdParameter ??= AllocateParameter(_containerAppEnvironmentContext.ManagedIdentityId);
Expand Down Expand Up @@ -986,15 +999,15 @@ public static IManifestExpressionProvider GetSecretOutputKeyVault(AzureBicepReso
new SecretOutputExpression(resource);
}

private sealed class ProjectResourceExpression(ProjectResource projectResource, string propertyExpression) : IManifestExpressionProvider
private sealed class ResourceExpression(IResource resource, string propertyExpression) : IManifestExpressionProvider
{
public string ValueExpression => $"{{{projectResource.Name}.{propertyExpression}}}";
public string ValueExpression => $"{{{resource.Name}.{propertyExpression}}}";

public static IManifestExpressionProvider GetContainerImageExpression(ProjectResource p) =>
new ProjectResourceExpression(p, "containerImage");
public static IManifestExpressionProvider GetContainerImageExpression(IResource p) =>
new ResourceExpression(p, "containerImage");

public static IManifestExpressionProvider GetContainerPortExpression(ProjectResource p) =>
new ProjectResourceExpression(p, "containerPort");
public static IManifestExpressionProvider GetContainerPortExpression(IResource p) =>
new ResourceExpression(p, "containerPort");
}

/// <summary>
Expand Down
33 changes: 29 additions & 4 deletions src/Aspire.Hosting/ApplicationModel/DockerfileBuildAnnotation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,36 @@

namespace Aspire.Hosting.ApplicationModel;

internal class DockerfileBuildAnnotation(string contextPath, string dockerfilePath, string? stage) : IResourceAnnotation
/// <summary>
/// Represents an annotation for customizing a Dockerfile build.
/// </summary>
/// <param name="contextPath">The path to the context directory for the build. </param>
/// <param name="dockerfilePath">The path to the Dockerfile to use for the build.</param>
/// <param name="stage">The name of the build stage to use for the build.</param>
public class DockerfileBuildAnnotation(string contextPath, string dockerfilePath, string? stage) : IResourceAnnotation
{
/// <summary>
/// Gets the path to the context directory for the build.
/// </summary>
public string ContextPath => contextPath;
public string DockerfilePath = dockerfilePath;

/// <summary>
/// Gets the path to the Dockerfile to use for the build.
/// </summary>
public string DockerfilePath => dockerfilePath;

/// <summary>
/// Gets the name of the build stage to use for the build.
/// </summary>
public string? Stage => stage;
public Dictionary<string, object> BuildArguments { get; } = new();
public Dictionary<string, object> BuildSecrets { get; } = new();

/// <summary>
/// Gets the arguments to pass to the build.
/// </summary>
public Dictionary<string, object> BuildArguments { get; } = [];

/// <summary>
/// Gets the secrets to pass to the build.
/// </summary>
public Dictionary<string, object> BuildSecrets { get; } = [];
}
7 changes: 7 additions & 0 deletions src/Aspire.Hosting/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
#nullable enable
Aspire.Hosting.ApplicationModel.ContainerLifetime.Session = 0 -> Aspire.Hosting.ApplicationModel.ContainerLifetime
Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.HealthStatus.get -> Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus?
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.BuildArguments.get -> System.Collections.Generic.Dictionary<string!, object!>!
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.BuildSecrets.get -> System.Collections.Generic.Dictionary<string!, object!>!
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.ContextPath.get -> string!
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.DockerfileBuildAnnotation(string! contextPath, string! dockerfilePath, string? stage) -> void
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.DockerfilePath.get -> string!
Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation.Stage.get -> string?
Aspire.Hosting.ApplicationModel.EndpointNameAttribute
Aspire.Hosting.ApplicationModel.EndpointNameAttribute.EndpointNameAttribute() -> void
Aspire.Hosting.ApplicationModel.HealthReportSnapshot
Expand Down
108 changes: 108 additions & 0 deletions tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,114 @@ param outputs_azure_container_apps_environment_id string
Assert.Equal(expectedBicep, bicep);
}

[Fact]
public async Task AddDockerfileWithAppsInfrastructureAddsDeploymentTargetWithContainerAppToContainerResources()
{
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);

builder.AddAzureContainerAppsInfrastructure();

var directory = Directory.CreateTempSubdirectory(".aspire-test");

// Contents of the Dockerfile are not important for this test
File.WriteAllText(Path.Combine(directory.FullName, "Dockerfile"), "");

builder.AddDockerfile("api", directory.FullName);

using var app = builder.Build();

await ExecuteBeforeStartHooksAsync(app, default);

var model = app.Services.GetRequiredService<DistributedApplicationModel>();

var container = Assert.Single(model.GetContainerResources());

container.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var target);

var resource = target?.DeploymentTarget as AzureProvisioningResource;

Assert.NotNull(resource);

var (manifest, bicep) = await ManifestUtils.GetManifestWithBicep(resource);

var m = manifest.ToString();

var expectedManifest =
"""
{
"type": "azure.bicep.v0",
"path": "api.module.bicep",
"params": {
"outputs_azure_container_registry_managed_identity_id": "{.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}",
"outputs_managed_identity_client_id": "{.outputs.MANAGED_IDENTITY_CLIENT_ID}",
"outputs_azure_container_apps_environment_id": "{.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}",
"outputs_azure_container_registry_endpoint": "{.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}",
"api_containerimage": "{api.containerImage}"
}
}
""";

Assert.Equal(expectedManifest, m);

var expectedBicep =
"""
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location

param outputs_azure_container_registry_managed_identity_id string

param outputs_managed_identity_client_id string

param outputs_azure_container_apps_environment_id string

param outputs_azure_container_registry_endpoint string

param api_containerimage string

resource api 'Microsoft.App/containerApps@2024-03-01' = {
name: 'api'
location: location
properties: {
configuration: {
activeRevisionsMode: 'Single'
registries: [
{
server: outputs_azure_container_registry_endpoint
identity: outputs_azure_container_registry_managed_identity_id
}
]
}
environmentId: outputs_azure_container_apps_environment_id
template: {
containers: [
{
image: api_containerimage
name: 'api'
env: [
{
name: 'AZURE_CLIENT_ID'
value: outputs_managed_identity_client_id
}
]
}
]
scale: {
minReplicas: 1
}
}
}
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${outputs_azure_container_registry_managed_identity_id}': { }
}
}
}
""";
output.WriteLine(bicep);
Assert.Equal(expectedBicep, bicep);
}

[Fact]
public async Task AddContainerAppsInfrastructureAddsDeploymentTargetWithContainerAppToProjectResources()
{
Expand Down