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
Expand Up @@ -29,6 +29,8 @@ record struct EndpointMapping(string Scheme, BicepValue<string> Host, int Port,
public Dictionary<string, object> EnvironmentVariables { get; } = [];
public List<object> Args { get; } = [];

private int? _targetPort;

private AzureResourceInfrastructure? _infrastructure;
public AzureResourceInfrastructure Infra => _infrastructure ?? throw new InvalidOperationException("Infra is not set");

Expand Down Expand Up @@ -89,6 +91,15 @@ private void ProcessEndpoints()
throw new NotSupportedException($"The endpoint(s) {string.Join(", ", unsupportedEndpoints.Select(e => $"'{e.Name}'"))} on resource '{resource.Name}' specifies an unsupported scheme. Only http and https are supported in App Service.");
}

// App Service supports only one target port
var targetPortEndpoints = endpoints.Where(e => e.IsExternal && e.TargetPort is not null).Select(e => e.TargetPort).Distinct().ToList();
if (targetPortEndpoints.Count > 1)
{
throw new NotSupportedException("App Service does not support resources with multiple external endpoints.");
}

_targetPort = targetPortEndpoints.FirstOrDefault();

foreach (var endpoint in endpoints)
{
if (!endpoint.IsExternal)
Expand Down Expand Up @@ -266,7 +277,11 @@ public void BuildWebSite(AzureResourceInfrastructure infra)
IsMain = true
};

infra.Add(mainContainer);
if (_targetPort is not null)
{
mainContainer.TargetPort = _targetPort.Value.ToString(CultureInfo.InvariantCulture);
webSite.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = "WEBSITES_PORT", Value = _targetPort.Value.ToString(CultureInfo.InvariantCulture) });
}

foreach (var kv in EnvironmentVariables)
{
Expand Down Expand Up @@ -301,9 +316,11 @@ static FunctionCallExpression Join(BicepExpression args, string delimeter) =>

var arrayExpression = new ArrayExpression([.. args.Select(a => a.Compile())]);

webSite.SiteConfig.AppCommandLine = Join(arrayExpression, " ");
mainContainer.StartUpCommand = Join(arrayExpression, " ");
}

infra.Add(mainContainer);

var id = BicepFunction.Interpolate($"{acrMidParameter}").Compile().ToString();
webSite.Identity.UserAssignedIdentities[id] = new UserAssignedIdentityDetails();

Expand Down
131 changes: 131 additions & 0 deletions tests/Aspire.Hosting.Azure.Tests/AzureAppServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,137 @@ await Verify(manifest.ToString(), "json")
.AppendContentAsFile(bicep, "bicep");
}

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

builder.AddAzureAppServiceEnvironment("env");

// Add 2 projects with endpoints
var project1 = builder.AddProject<Project>("project1", launchProfileName: null)
.WithHttpEndpoint()
.WithExternalHttpEndpoints();

var project2 = builder.AddProject<Project>("project2", launchProfileName: null)
.WithHttpEndpoint()
.WithArgs("--myarg", "myvalue")
.WithExternalHttpEndpoints()
.WithReference(project1);

using var app = builder.Build();

await ExecuteBeforeStartHooksAsync(app, default);

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

project2.Resource.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var target);

var resource = target?.DeploymentTarget as AzureProvisioningResource;

Assert.NotNull(resource);

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

await Verify(manifest.ToString(), "json")
.AppendContentAsFile(bicep, "bicep");
}

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

builder.AddAzureAppServiceEnvironment("env");

// Add 2 projects with endpoints
var project1 = builder.AddProject<Project>("project1", launchProfileName: null)
.WithHttpsEndpoint(targetPort:8000)
.WithHttpEndpoint(targetPort: 8000)
.WithExternalHttpEndpoints();

var project2 = builder.AddProject<Project>("project2", launchProfileName: null)
.WithHttpEndpoint(targetPort:9000)
.WithExternalHttpEndpoints()
.WithReference(project1);

using var app = builder.Build();

await ExecuteBeforeStartHooksAsync(app, default);

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

project2.Resource.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var target);

var resource = target?.DeploymentTarget as AzureProvisioningResource;

Assert.NotNull(resource);

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

await Verify(manifest.ToString(), "json")
.AppendContentAsFile(bicep, "bicep");
}

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

builder.AddAzureAppServiceEnvironment("env");

// Add 2 projects with endpoints
var project1 = builder.AddProject<Project>("project1", launchProfileName: null)
.WithExternalHttpEndpoints();

var project2 = builder.AddProject<Project>("project2", launchProfileName: null)
.WithHttpsEndpoint(targetPort: 8000)
.WithHttpEndpoint(targetPort: 8000)
.WithExternalHttpEndpoints()
.WithReference(project1);

using var app = builder.Build();

await ExecuteBeforeStartHooksAsync(app, default);

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

project2.Resource.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var target);

var resource = target?.DeploymentTarget as AzureProvisioningResource;

Assert.NotNull(resource);

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

await Verify(manifest.ToString(), "json")
.AppendContentAsFile(bicep, "bicep");
}

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

builder.AddAzureAppServiceEnvironment("env");

// Add 2 projects with endpoints
var project1 = builder.AddProject<Project>("project1", launchProfileName: null)
.WithExternalHttpEndpoints();

var project2 = builder.AddProject<Project>("project2", launchProfileName: null)
.WithHttpsEndpoint(targetPort: 8000)
.WithHttpEndpoint(targetPort: 8800)
.WithExternalHttpEndpoints()
.WithReference(project1);

using var app = builder.Build();

var ex = await Assert.ThrowsAsync<NotSupportedException>(() => ExecuteBeforeStartHooksAsync(app, default));

Assert.Equal("App Service does not support resources with multiple external endpoints.", ex.Message);
}

private static Task<(JsonNode ManifestNode, string BicepText)> GetManifestWithBicep(IResource resource) =>
AzureManifestUtils.GetManifestWithBicep(resource, skipPreparer: true);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location

param env_outputs_azure_container_registry_endpoint string

param env_outputs_planid string

param env_outputs_azure_container_registry_managed_identity_id string

param env_outputs_azure_container_registry_managed_identity_client_id string

param project2_containerimage string

param project2_containerport string

param env_outputs_azure_app_service_dashboard_uri string

param env_outputs_azure_website_contributor_managed_identity_id string

param env_outputs_azure_website_contributor_managed_identity_principal_id string

resource mainContainer 'Microsoft.Web/sites/sitecontainers@2024-11-01' = {
name: 'main'
properties: {
authType: 'UserAssigned'
image: project2_containerimage
isMain: true
startUpCommand: join([
'--myarg'
'myvalue'
], ' ')
userManagedIdentityClientId: env_outputs_azure_container_registry_managed_identity_client_id
}
parent: webapp
}

resource webapp 'Microsoft.Web/sites@2024-11-01' = {
name: take('${toLower('project2')}-${uniqueString(resourceGroup().id)}', 60)
location: location
properties: {
serverFarmId: env_outputs_planid
siteConfig: {
numberOfWorkers: 30
linuxFxVersion: 'SITECONTAINERS'
acrUseManagedIdentityCreds: true
acrUserManagedIdentityID: env_outputs_azure_container_registry_managed_identity_client_id
appSettings: [
{
name: 'OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES'
value: 'true'
}
{
name: 'OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES'
value: 'true'
}
{
name: 'OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY'
value: 'in_memory'
}
{
name: 'ASPNETCORE_FORWARDEDHEADERS_ENABLED'
value: 'true'
}
{
name: 'HTTP_PORTS'
value: project2_containerport
}
{
name: 'services__project1__http__0'
value: 'http://${take('${toLower('project1')}-${uniqueString(resourceGroup().id)}', 60)}.azurewebsites.net'
}
{
name: 'ASPIRE_ENVIRONMENT_NAME'
value: 'env'
}
{
name: 'OTEL_SERVICE_NAME'
value: 'project2'
}
{
name: 'OTEL_EXPORTER_OTLP_PROTOCOL'
value: 'grpc'
}
{
name: 'OTEL_EXPORTER_OTLP_ENDPOINT'
value: 'http://localhost:6001'
}
{
name: 'WEBSITE_ENABLE_ASPIRE_OTEL_SIDECAR'
value: 'true'
}
{
name: 'OTEL_COLLECTOR_URL'
value: env_outputs_azure_app_service_dashboard_uri
}
{
name: 'OTEL_CLIENT_ID'
value: env_outputs_azure_container_registry_managed_identity_client_id
}
]
}
}
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${env_outputs_azure_container_registry_managed_identity_id}': { }
}
}
}

resource project2_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(webapp.id, env_outputs_azure_website_contributor_managed_identity_id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772'))
properties: {
principalId: env_outputs_azure_website_contributor_managed_identity_principal_id
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')
principalType: 'ServicePrincipal'
}
scope: webapp
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"type": "azure.bicep.v0",
"path": "project2.module.bicep",
"params": {
"env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}",
"env_outputs_planid": "{env.outputs.planId}",
"env_outputs_azure_container_registry_managed_identity_id": "{env.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}",
"env_outputs_azure_container_registry_managed_identity_client_id": "{env.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_CLIENT_ID}",
"project2_containerimage": "{project2.containerImage}",
"project2_containerport": "{project2.containerPort}",
"env_outputs_azure_app_service_dashboard_uri": "{env.outputs.AZURE_APP_SERVICE_DASHBOARD_URI}",
"env_outputs_azure_website_contributor_managed_identity_id": "{env.outputs.AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_ID}",
"env_outputs_azure_website_contributor_managed_identity_principal_id": "{env.outputs.AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_PRINCIPAL_ID}"
}
}
Loading