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 @@ -3,6 +3,7 @@

#pragma warning disable AZPROVISION001 // Azure.Provisioning.Network is experimental

using Aspire.Hosting.Azure;
using Azure.Provisioning.Network;

var builder = DistributedApplication.CreateBuilder(args);
Expand All @@ -13,17 +14,17 @@
var vnet = builder.AddAzureVirtualNetwork("vnet");

var containerAppsSubnet = vnet.AddSubnet("container-apps", "10.0.0.0/23")
.AllowInbound(port: "443", from: "AzureLoadBalancer", protocol: SecurityRuleProtocol.Tcp)
.DenyInbound(from: "VirtualNetwork")
.DenyInbound(from: "Internet");
.AllowInbound(port: "443", from: AzureServiceTags.AzureLoadBalancer, protocol: SecurityRuleProtocol.Tcp)
.DenyInbound(from: AzureServiceTags.VirtualNetwork)
.DenyInbound(from: AzureServiceTags.Internet);

// Create a NAT Gateway for deterministic outbound IP on the ACA subnet
var natGateway = builder.AddNatGateway("nat");
containerAppsSubnet.WithNatGateway(natGateway);

var privateEndpointsSubnet = vnet.AddSubnet("private-endpoints", "10.0.2.0/27")
.AllowInbound(port: "443", from: "VirtualNetwork", protocol: SecurityRuleProtocol.Tcp)
.DenyInbound(from: "Internet");
.AllowInbound(port: "443", from: AzureServiceTags.VirtualNetwork, protocol: SecurityRuleProtocol.Tcp)
.DenyInbound(from: AzureServiceTags.Internet);

// Configure the Container App Environment to use the VNet
builder.AddAzureContainerAppEnvironment("env")
Expand Down
5 changes: 5 additions & 0 deletions src/Aspire.Cli/Backchannel/IAuxiliaryBackchannelMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ internal interface IAuxiliaryBackchannelMonitor
/// </summary>
IAppHostAuxiliaryBackchannel? SelectedConnection { get; }

/// <summary>
/// Gets the AppHost path of the currently resolved connection, or <c>null</c> if no connection is available.
/// </summary>
string? ResolvedAppHostPath => SelectedConnection?.AppHostInfo?.AppHostPath;

/// <summary>
/// Gets all connections that are within the scope of the specified working directory.
/// </summary>
Expand Down
6 changes: 3 additions & 3 deletions src/Aspire.Cli/Mcp/McpResourceToolRefreshService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal sealed class McpResourceToolRefreshService : IMcpResourceToolRefreshSer
private McpServer? _server;
private Dictionary<string, ResourceToolEntry> _resourceToolMap = new(StringComparer.Ordinal);
private bool _invalidated = true;
private string? _selectedAppHostPath;
private string? _lastRefreshedAppHostPath;

public McpResourceToolRefreshService(
IAuxiliaryBackchannelMonitor auxiliaryBackchannelMonitor,
Expand All @@ -35,7 +35,7 @@ public bool TryGetResourceToolMap(out IReadOnlyDictionary<string, ResourceToolEn
{
lock (_lock)
{
if (_invalidated || _selectedAppHostPath != _auxiliaryBackchannelMonitor.SelectedAppHostPath)
if (_invalidated || _lastRefreshedAppHostPath != _auxiliaryBackchannelMonitor.ResolvedAppHostPath)
{
resourceToolMap = null!;
return false;
Expand Down Expand Up @@ -150,7 +150,7 @@ public async Task SendToolsListChangedNotificationAsync(CancellationToken cancel
}

_resourceToolMap = refreshedMap;
_selectedAppHostPath = selectedAppHostPath;
_lastRefreshedAppHostPath = selectedAppHostPath;
_invalidated = false;
return (_resourceToolMap, changed);
}
Expand Down
111 changes: 111 additions & 0 deletions src/Aspire.Hosting.Azure.Network/AzureServiceTags.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.Azure;

/// <summary>
/// Provides well-known Azure service tags that can be used as source or destination address prefixes
/// in network security group rules.
/// </summary>
/// <remarks>
/// <para>
/// Service tags represent a group of IP address prefixes from a given Azure service. Microsoft manages the
/// address prefixes encompassed by each tag and automatically updates them as addresses change.
/// </para>
/// <para>
/// These tags can be used with the <c>from</c> and <c>to</c> parameters of methods such as
/// <see cref="AzureVirtualNetworkExtensions.AllowInbound"/>, <see cref="AzureVirtualNetworkExtensions.DenyInbound"/>,
/// <see cref="AzureVirtualNetworkExtensions.AllowOutbound"/>, <see cref="AzureVirtualNetworkExtensions.DenyOutbound"/>,
/// or with the <see cref="AzureSecurityRule.SourceAddressPrefix"/> and <see cref="AzureSecurityRule.DestinationAddressPrefix"/> properties.
/// </para>
/// </remarks>
/// <example>
/// Use service tags when configuring network security rules:
/// <code>
/// var subnet = vnet.AddSubnet("web", "10.0.1.0/24")
/// .AllowInbound(port: "443", from: AzureServiceTags.AzureLoadBalancer, protocol: SecurityRuleProtocol.Tcp)
/// .DenyInbound(from: AzureServiceTags.Internet);
/// </code>
/// </example>
public static class AzureServiceTags
{
/// <summary>
/// Represents the Internet address space, including all publicly routable IP addresses.
/// </summary>
public const string Internet = nameof(Internet);

/// <summary>
/// Represents the address space for the virtual network, including all connected address spaces,
/// all connected on-premises address spaces, and peered virtual networks.
/// </summary>
public const string VirtualNetwork = nameof(VirtualNetwork);

/// <summary>
/// Represents the Azure infrastructure load balancer. This tag is commonly used to allow
/// health probe traffic from Azure.
/// </summary>
public const string AzureLoadBalancer = nameof(AzureLoadBalancer);

/// <summary>
/// Represents Azure Traffic Manager probe IP addresses.
/// </summary>
public const string AzureTrafficManager = nameof(AzureTrafficManager);

/// <summary>
/// Represents the Azure Storage service. This tag does not include specific Storage accounts;
/// it covers all Azure Storage IP addresses.
/// </summary>
public const string Storage = nameof(Storage);

/// <summary>
/// Represents Azure SQL Database, Azure Database for MySQL, Azure Database for PostgreSQL,
/// Azure Database for MariaDB, and Azure Synapse Analytics.
/// </summary>
public const string Sql = nameof(Sql);

/// <summary>
/// Represents Azure Cosmos DB service addresses.
/// </summary>
public const string AzureCosmosDB = nameof(AzureCosmosDB);

/// <summary>
/// Represents Azure Key Vault service addresses.
/// </summary>
public const string AzureKeyVault = nameof(AzureKeyVault);

/// <summary>
/// Represents Azure Event Hubs service addresses.
/// </summary>
public const string EventHub = nameof(EventHub);

/// <summary>
/// Represents Azure Service Bus service addresses.
/// </summary>
public const string ServiceBus = nameof(ServiceBus);

/// <summary>
/// Represents Azure Container Registry service addresses.
/// </summary>
public const string AzureContainerRegistry = nameof(AzureContainerRegistry);

/// <summary>
/// Represents Azure App Service and Azure Functions service addresses.
/// </summary>
public const string AppService = nameof(AppService);

/// <summary>
/// Represents Microsoft Entra ID (formerly Azure Active Directory) service addresses.
/// </summary>
public const string AzureActiveDirectory = nameof(AzureActiveDirectory);

/// <summary>
/// Represents Azure Monitor service addresses, including Log Analytics, Application Insights,
/// and Azure Monitor metrics.
/// </summary>
public const string AzureMonitor = nameof(AzureMonitor);

/// <summary>
/// Represents the Gateway Manager service, used for VPN Gateway and Application Gateway management traffic.
/// </summary>
public const string GatewayManager = nameof(GatewayManager);
}
Original file line number Diff line number Diff line change
Expand Up @@ -361,8 +361,8 @@ public static IResourceBuilder<AzureSubnetResource> WithNetworkSecurityGroup(
/// This example allows HTTPS traffic from the Azure Load Balancer:
/// <code>
/// var subnet = vnet.AddSubnet("web", "10.0.1.0/24")
/// .AllowInbound(port: "443", from: "AzureLoadBalancer", protocol: SecurityRuleProtocol.Tcp)
/// .DenyInbound(from: "Internet");
/// .AllowInbound(port: "443", from: AzureServiceTags.AzureLoadBalancer, protocol: SecurityRuleProtocol.Tcp)
/// .DenyInbound(from: AzureServiceTags.Internet);
/// </code>
/// </example>
public static IResourceBuilder<AzureSubnetResource> AllowInbound(
Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Hosting.Azure.Network/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ Add security rules to control traffic flow on subnets using shorthand methods:
```csharp
var vnet = builder.AddAzureVirtualNetwork("vnet");
var subnet = vnet.AddSubnet("web", "10.0.1.0/24")
.AllowInbound(port: "443", from: "AzureLoadBalancer", protocol: SecurityRuleProtocol.Tcp)
.DenyInbound(from: "Internet");
.AllowInbound(port: "443", from: AzureServiceTags.AzureLoadBalancer, protocol: SecurityRuleProtocol.Tcp)
.DenyInbound(from: AzureServiceTags.Internet);
```

An NSG is automatically created when shorthand methods are used. Priority auto-increments (100, 200, 300...) and rule names are auto-generated.
Expand Down
32 changes: 32 additions & 0 deletions src/Aspire.Hosting/Publishing/BuildImageSecretValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;

namespace Aspire.Hosting.Publishing;

/// <summary>
/// Specifies the type of a build secret.
/// </summary>
[Experimental("ASPIRECONTAINERRUNTIME001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public enum BuildImageSecretType
{
/// <summary>
/// The secret value is provided via an environment variable.
/// </summary>
Environment,

/// <summary>
/// The secret value is a file path.
/// </summary>
File
}

/// <summary>
/// Represents a resolved build secret with its value and type.
/// </summary>
/// <param name="Value">The resolved secret value. For <see cref="BuildImageSecretType.Environment"/> secrets, this is the secret content.
/// For <see cref="BuildImageSecretType.File"/> secrets, this is the file path.</param>
/// <param name="Type">The type of the build secret, indicating whether it is environment-based or file-based.</param>
[Experimental("ASPIRECONTAINERRUNTIME001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public record BuildImageSecretValue(string? Value, BuildImageSecretType Type);
14 changes: 9 additions & 5 deletions src/Aspire.Hosting/Publishing/ContainerRuntimeBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ protected ContainerRuntimeBase(ILogger<TLogger> logger)

public abstract Task<bool> CheckIfRunningAsync(CancellationToken cancellationToken);

public abstract Task BuildImageAsync(string contextPath, string dockerfilePath, ContainerImageBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, string?> buildSecrets, string? stage, CancellationToken cancellationToken);
public abstract Task BuildImageAsync(string contextPath, string dockerfilePath, ContainerImageBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, BuildImageSecretValue> buildSecrets, string? stage, CancellationToken cancellationToken);

public virtual async Task TagImageAsync(string localImageName, string targetImageName, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -241,18 +241,22 @@ protected static string BuildArgumentsString(Dictionary<string, string?> buildAr
/// <param name="buildSecrets">The build secrets to include.</param>
/// <param name="requireValue">Whether to require a non-null value for secrets (default: false).</param>
/// <returns>A string containing the formatted build secrets.</returns>
protected static string BuildSecretsString(Dictionary<string, string?> buildSecrets, bool requireValue = false)
internal static string BuildSecretsString(Dictionary<string, BuildImageSecretValue> buildSecrets, bool requireValue = false)
{
var result = string.Empty;
foreach (var buildSecret in buildSecrets)
{
if (requireValue && buildSecret.Value is null)
if (buildSecret.Value.Type == BuildImageSecretType.File)
{
result += $" --secret \"id={buildSecret.Key}\"";
result += $" --secret \"id={buildSecret.Key},type=file,src={buildSecret.Value.Value}\"";
}
else if (requireValue && buildSecret.Value.Value is null)
{
result += $" --secret \"id={buildSecret.Key},type=env\"";
}
else
{
result += $" --secret \"id={buildSecret.Key},env={buildSecret.Key.ToUpperInvariant()}\"";
result += $" --secret \"id={buildSecret.Key},type=env,env={buildSecret.Key.ToUpperInvariant()}\"";
}
}
return result;
Expand Down
11 changes: 6 additions & 5 deletions src/Aspire.Hosting/Publishing/DockerContainerRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

#pragma warning disable ASPIREPIPELINES003
#pragma warning disable ASPIRECONTAINERRUNTIME001

using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Dcp.Process;
Expand All @@ -17,7 +18,7 @@ public DockerContainerRuntime(ILogger<DockerContainerRuntime> logger) : base(log

protected override string RuntimeExecutable => "docker";
public override string Name => "Docker";
private async Task<int> RunDockerBuildAsync(string contextPath, string dockerfilePath, ContainerImageBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, string?> buildSecrets, string? stage, CancellationToken cancellationToken)
private async Task<int> RunDockerBuildAsync(string contextPath, string dockerfilePath, ContainerImageBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, BuildImageSecretValue> buildSecrets, string? stage, CancellationToken cancellationToken)
{
var imageName = !string.IsNullOrEmpty(options?.Tag)
? $"{options.ImageName}:{options.Tag}"
Expand Down Expand Up @@ -107,12 +108,12 @@ private async Task<int> RunDockerBuildAsync(string contextPath, string dockerfil
InheritEnv = true,
};

// Add build secrets as environment variables
// Add build secrets as environment variables (only for environment-type secrets)
foreach (var buildSecret in buildSecrets)
{
if (buildSecret.Value is not null)
if (buildSecret.Value.Type == BuildImageSecretType.Environment && buildSecret.Value.Value is not null)
{
spec.EnvironmentVariables[buildSecret.Key.ToUpperInvariant()] = buildSecret.Value;
spec.EnvironmentVariables[buildSecret.Key.ToUpperInvariant()] = buildSecret.Value.Value;
}
}

Expand Down Expand Up @@ -145,7 +146,7 @@ private async Task<int> RunDockerBuildAsync(string contextPath, string dockerfil
}
}

public override async Task BuildImageAsync(string contextPath, string dockerfilePath, ContainerImageBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, string?> buildSecrets, string? stage, CancellationToken cancellationToken)
public override async Task BuildImageAsync(string contextPath, string dockerfilePath, ContainerImageBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, BuildImageSecretValue> buildSecrets, string? stage, CancellationToken cancellationToken)
{
// Normalize the context path to handle trailing slashes and relative paths
var normalizedContextPath = Path.GetFullPath(contextPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/Publishing/IContainerRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public interface IContainerRuntime
/// <param name="buildSecrets">Build secrets to pass to the build process.</param>
/// <param name="stage">The target build stage.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
Task BuildImageAsync(string contextPath, string dockerfilePath, ContainerImageBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, string?> buildSecrets, string? stage, CancellationToken cancellationToken);
Task BuildImageAsync(string contextPath, string dockerfilePath, ContainerImageBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, BuildImageSecretValue> buildSecrets, string? stage, CancellationToken cancellationToken);

/// <summary>
/// Tags a container image with a new name.
Expand Down
11 changes: 6 additions & 5 deletions src/Aspire.Hosting/Publishing/PodmanContainerRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

#pragma warning disable ASPIREPIPELINES003
#pragma warning disable ASPIRECONTAINERRUNTIME001

using Microsoft.Extensions.Logging;

Expand All @@ -15,7 +16,7 @@ public PodmanContainerRuntime(ILogger<PodmanContainerRuntime> logger) : base(log

protected override string RuntimeExecutable => "podman";
public override string Name => "Podman";
private async Task<int> RunPodmanBuildAsync(string contextPath, string dockerfilePath, ContainerImageBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, string?> buildSecrets, string? stage, CancellationToken cancellationToken)
private async Task<int> RunPodmanBuildAsync(string contextPath, string dockerfilePath, ContainerImageBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, BuildImageSecretValue> buildSecrets, string? stage, CancellationToken cancellationToken)
{
var imageName = !string.IsNullOrEmpty(options?.Tag)
? $"{options.ImageName}:{options.Tag}"
Expand Down Expand Up @@ -60,13 +61,13 @@ private async Task<int> RunPodmanBuildAsync(string contextPath, string dockerfil

arguments += $" \"{contextPath}\"";

// Prepare environment variables for build secrets
// Prepare environment variables for build secrets (only for environment-type secrets)
var environmentVariables = new Dictionary<string, string>();
foreach (var buildSecret in buildSecrets)
{
if (buildSecret.Value is not null)
if (buildSecret.Value.Type == BuildImageSecretType.Environment && buildSecret.Value.Value is not null)
{
environmentVariables[buildSecret.Key.ToUpperInvariant()] = buildSecret.Value;
environmentVariables[buildSecret.Key.ToUpperInvariant()] = buildSecret.Value.Value;
}
}

Expand All @@ -79,7 +80,7 @@ private async Task<int> RunPodmanBuildAsync(string contextPath, string dockerfil
environmentVariables).ConfigureAwait(false);
}

public override async Task BuildImageAsync(string contextPath, string dockerfilePath, ContainerImageBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, string?> buildSecrets, string? stage, CancellationToken cancellationToken)
public override async Task BuildImageAsync(string contextPath, string dockerfilePath, ContainerImageBuildOptions? options, Dictionary<string, string?> buildArguments, Dictionary<string, BuildImageSecretValue> buildSecrets, string? stage, CancellationToken cancellationToken)
{
var exitCode = await RunPodmanBuildAsync(
contextPath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,10 +440,12 @@ private async Task BuildContainerImageFromDockerfileAsync(IResource resource, Do
}

// Resolve build secrets
var resolvedBuildSecrets = new Dictionary<string, string?>();
var resolvedBuildSecrets = new Dictionary<string, BuildImageSecretValue>();
foreach (var buildSecret in dockerfileBuildAnnotation.BuildSecrets)
{
resolvedBuildSecrets[buildSecret.Key] = await ResolveValue(buildSecret.Value, cancellationToken).ConfigureAwait(false);
var secretType = buildSecret.Value is FileInfo ? BuildImageSecretType.File : BuildImageSecretType.Environment;
var resolvedValue = await ResolveValue(buildSecret.Value, cancellationToken).ConfigureAwait(false);
resolvedBuildSecrets[buildSecret.Key] = new BuildImageSecretValue(resolvedValue, secretType);
}

// ensure outputPath is created if specified since docker/podman won't create it for us
Expand Down
Loading
Loading