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
5 changes: 4 additions & 1 deletion src/Aspire.Hosting.Azure/AzureBicepResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,10 @@ public virtual void WriteToManifest(ManifestPublishingContext context)
if (Scope is not null)
{
context.Writer.WriteStartObject("scope");
WriteScopeValue(context, "resourceGroup", Scope.ResourceGroup);
if (Scope.HasResourceGroup)
{
WriteScopeValue(context, "resourceGroup", Scope.ResourceGroup);
}
WriteScopeValue(context, "subscription", Scope.Subscription);
if (Scope.IsTenantScope)
{
Expand Down
48 changes: 37 additions & 11 deletions src/Aspire.Hosting.Azure/AzureBicepResourceScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ namespace Aspire.Hosting.Azure;
/// </summary>
public sealed class AzureBicepResourceScope
{
private readonly object? _resourceGroup;

/// <summary>
/// Initializes a new instance of the <see cref="AzureBicepResourceScope"/> class with a resource group scope.
/// </summary>
Expand All @@ -16,7 +18,7 @@ public AzureBicepResourceScope(object resourceGroup)
{
ArgumentNullException.ThrowIfNull(resourceGroup);

ResourceGroup = resourceGroup;
_resourceGroup = resourceGroup;
}

/// <summary>
Expand All @@ -31,38 +33,54 @@ public AzureBicepResourceScope(object resourceGroup, object subscription) : this
Subscription = subscription;
}

private AzureBicepResourceScope(object? resourceGroup, object? subscription, bool isTenantScope)
private AzureBicepResourceScope(ScopeKind scopeKind)
{
if (scopeKind is not ScopeKind.Tenant)
{
throw new ArgumentOutOfRangeException(nameof(scopeKind));
}

IsTenantScope = true;
}

private AzureBicepResourceScope(ScopeKind scopeKind, object subscription)
{
ResourceGroup = resourceGroup;
if (scopeKind is not ScopeKind.Subscription)
{
throw new ArgumentOutOfRangeException(nameof(scopeKind));
}

ArgumentNullException.ThrowIfNull(subscription);

Subscription = subscription;
IsTenantScope = isTenantScope;
}

/// <summary>
/// Creates a scope for subscription-level resources.
/// </summary>
/// <param name="subscription">The subscription identifier for subscription-level resources.</param>
/// <returns>A new <see cref="AzureBicepResourceScope"/> scoped to the subscription.</returns>
public static AzureBicepResourceScope ForSubscription(object subscription)
public static AzureBicepResourceScope CreateForSubscription(object subscription)
{
ArgumentNullException.ThrowIfNull(subscription);

return new AzureBicepResourceScope(resourceGroup: null, subscription, isTenantScope: false);
return new AzureBicepResourceScope(ScopeKind.Subscription, subscription);
}

/// <summary>
/// Creates a scope for tenant-level resources in the current tenant.
/// </summary>
/// <returns>A new <see cref="AzureBicepResourceScope"/> scoped to the current tenant.</returns>
public static AzureBicepResourceScope ForTenant()
public static AzureBicepResourceScope CreateForTenant()
{
return new AzureBicepResourceScope(resourceGroup: null, subscription: null, isTenantScope: true);
return new AzureBicepResourceScope(ScopeKind.Tenant);
}

/// <summary>
/// Represents the resource group to encode in the scope.
/// </summary>
public object? ResourceGroup { get; }
/// <exception cref="InvalidOperationException">The scope does not target a resource group.</exception>
public object ResourceGroup => _resourceGroup ?? throw new InvalidOperationException("The Azure Bicep resource scope does not target a resource group.");

/// <summary>
/// Represents the subscription to encode in the scope.
Expand All @@ -74,21 +92,29 @@ public static AzureBicepResourceScope ForTenant()
/// </summary>
public bool IsTenantScope { get; }

internal bool HasResourceGroup => _resourceGroup is not null;

internal static AzureBicepResourceScope? FromExistingResourceAnnotation(ExistingAzureResourceAnnotation annotation)
{
ArgumentNullException.ThrowIfNull(annotation);

if (annotation.IsTenantScope)
{
return ForTenant();
return CreateForTenant();
}

return (annotation.ResourceGroup, annotation.Subscription) switch
{
({ } resourceGroup, { } subscription) => new AzureBicepResourceScope(resourceGroup, subscription),
({ } resourceGroup, null) => new AzureBicepResourceScope(resourceGroup),
(null, { } subscription) => ForSubscription(subscription),
(null, { } subscription) => CreateForSubscription(subscription),
_ => null
};
}

private enum ScopeKind
{
Subscription,
Tenant
}
}
7 changes: 4 additions & 3 deletions src/Aspire.Hosting.Azure/AzureProvisioningResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ public static bool TryApplyExistingResourceAnnotation(IAzureResource aspireResou
private static bool ScopeEquals(AzureBicepResourceScope expected, AzureBicepResourceScope? actual)
{
return actual is not null &&
ScopeValueEquals(expected.ResourceGroup, actual.ResourceGroup) &&
expected.HasResourceGroup == actual.HasResourceGroup &&
(!expected.HasResourceGroup || ScopeValueEquals(expected.ResourceGroup, actual.ResourceGroup)) &&
ScopeValueEquals(expected.Subscription, actual.Subscription) &&
expected.IsTenantScope == actual.IsTenantScope;
}
Expand All @@ -220,7 +221,7 @@ private static BicepValue<string> CreateScopeExpression(AzureBicepResourceScope
return new FunctionCallExpression(new IdentifierExpression("tenant"));
}

if (scope.ResourceGroup is not null && scope.Subscription is not null)
if (scope.HasResourceGroup && scope.Subscription is not null)
{
return (scope.Subscription, scope.ResourceGroup) switch
{
Expand All @@ -232,7 +233,7 @@ private static BicepValue<string> CreateScopeExpression(AzureBicepResourceScope
};
}

if (scope.ResourceGroup is not null)
if (scope.HasResourceGroup)
{
return scope.ResourceGroup switch
{
Expand Down
23 changes: 14 additions & 9 deletions src/Aspire.Hosting.Azure/AzurePublishingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,10 @@ private async Task WriteAzureArtifactsOutputAsync(IReportingStep step, Distribut

if (resource.Scope is { } scope)
{
await VisitAsync(scope.ResourceGroup, MapParameterAsync, cancellationToken).ConfigureAwait(false);
if (scope.HasResourceGroup)
{
await VisitAsync(scope.ResourceGroup, MapParameterAsync, cancellationToken).ConfigureAwait(false);
}
await VisitAsync(scope.Subscription, MapParameterAsync, cancellationToken).ConfigureAwait(false);
}

Expand Down Expand Up @@ -263,27 +266,29 @@ BicepValue<string> GetScopeExpression(AzureBicepResource resource)
return new IdentifierExpression(rg.BicepIdentifier);
}

if (resource.Scope.IsTenantScope)
var scope = resource.Scope;

if (scope.IsTenantScope)
{
return new FunctionCallExpression(new IdentifierExpression("tenant"));
}

if (resource.Scope.ResourceGroup is not null && resource.Scope.Subscription is not null)
if (scope.HasResourceGroup && scope.Subscription is not null)
{
return new FunctionCallExpression(
new IdentifierExpression("resourceGroup"),
ResolveValue(Eval(resource.Scope.Subscription)).Compile(),
ResolveValue(Eval(resource.Scope.ResourceGroup)).Compile());
ResolveValue(Eval(scope.Subscription)).Compile(),
ResolveValue(Eval(scope.ResourceGroup)).Compile());
}

if (resource.Scope.ResourceGroup is not null)
if (scope.HasResourceGroup)
{
return new FunctionCallExpression(new IdentifierExpression("resourceGroup"), ResolveValue(Eval(resource.Scope.ResourceGroup)).Compile());
return new FunctionCallExpression(new IdentifierExpression("resourceGroup"), ResolveValue(Eval(scope.ResourceGroup)).Compile());
}

if (resource.Scope.Subscription is not null)
if (scope.Subscription is not null)
{
return new FunctionCallExpression(new IdentifierExpression("subscription"), ResolveValue(Eval(resource.Scope.Subscription)).Compile());
return new FunctionCallExpression(new IdentifierExpression("subscription"), ResolveValue(Eval(scope.Subscription)).Compile());
}

throw new InvalidOperationException("The Azure Bicep resource scope must specify a resource group, subscription, or tenant scope.");
Expand Down
5 changes: 4 additions & 1 deletion src/Aspire.Hosting.Azure/Provisioning/BicepUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ public static async Task SetScopeAsync(JsonObject scope, AzureBicepResource reso
return;
}

await SetScopeValueAsync(scope, "resourceGroup", targetScope.ResourceGroup, cancellationToken).ConfigureAwait(false);
if (targetScope.HasResourceGroup)
{
await SetScopeValueAsync(scope, "resourceGroup", targetScope.ResourceGroup, cancellationToken).ConfigureAwait(false);
}
await SetScopeValueAsync(scope, "subscription", targetScope.Subscription, cancellationToken).ConfigureAwait(false);
if (targetScope.IsTenantScope)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -511,16 +511,17 @@ public async Task GetOrCreateResourceAsync(AzureBicepResource resource, Provisio
var targetScope = BicepUtilities.GetExistingResourceScope(resource);
var isTenantScoped = targetScope?.IsTenantScope == true;
var isSubscriptionScoped = !isTenantScoped &&
targetScope is { Subscription: not null, ResourceGroup: null };
targetScope is { Subscription: not null, HasResourceGroup: false };

if (targetScope?.Subscription is { } existingSubscription)
{
var existingSubscriptionId = await ResolveScopeValueAsync(existingSubscription, cancellationToken).ConfigureAwait(false);
subscription = await context.ArmClient.GetSubscriptionAsync(existingSubscriptionId, cancellationToken).ConfigureAwait(false);
}

if (targetScope?.ResourceGroup is { } existingResourceGroup)
if (targetScope?.HasResourceGroup == true)
{
var existingResourceGroup = targetScope.ResourceGroup;
var existingResourceGroupName = await ResolveScopeValueAsync(existingResourceGroup, cancellationToken).ConfigureAwait(false);
var response = await subscription.GetResourceGroups().GetAsync(existingResourceGroupName, cancellationToken).ConfigureAwait(false);
resourceGroup = response.Value;
Expand Down
67 changes: 67 additions & 0 deletions src/Aspire.Hosting.Go/DelveServerOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// 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.Go;

/// <summary>
/// Configures the headless Delve debug server used for remote debugging of a Go application.
/// </summary>
/// <remarks>
/// <para>
/// The server listens on the loopback interface. By default, it accepts a single debugger client
/// and relies on Delve's default same-user connection policy.
/// </para>
/// <para>
/// Set <see cref="AcceptMultiClient"/> only when the server must remain available after a debugger
/// disconnects or when multiple debugger clients need to attach.
/// </para>
/// </remarks>
/// <example>
/// Configure a Delve server that continues the application immediately and accepts multiple debugger clients:
/// <code lang="csharp">
/// builder.AddGoApp("api", "../go-api")
/// .WithDelveServer(new DelveServerOptions
/// {
/// AcceptMultiClient = true,
/// ContinueOnStart = true
/// });
/// </code>
/// </example>
[AspireDto]
public sealed class DelveServerOptions
{
/// <summary>
/// Gets the TCP port on which Delve listens. The default is <c>2345</c>.
/// </summary>
public int Port { get; init; } = 2345;

/// <summary>
/// Gets a value indicating whether Delve accepts multiple debugger clients.
/// The default is <see langword="false"/>.
/// </summary>
public bool AcceptMultiClient { get; init; }

/// <summary>
/// Gets a value indicating whether Delve allows connections only from the same operating system user.
/// When <see langword="null"/>, Delve's default same-user policy is used.
/// </summary>
public bool? OnlySameUser { get; init; }

/// <summary>
/// Gets a value indicating whether Delve continues the application immediately after startup.
/// The default is <see langword="false"/>.
/// </summary>
public bool ContinueOnStart { get; init; }

/// <summary>
/// Gets a value indicating whether Delve server logging is enabled.
/// The default is <see langword="false"/>.
/// </summary>
public bool Log { get; init; }

/// <summary>
/// Gets the Delve logging components enabled when <see cref="Log"/> is <see langword="true"/>.
/// When <see langword="null"/> or empty, Delve uses its default logging components.
/// </summary>
public string? LogOutput { get; init; }
}
8 changes: 4 additions & 4 deletions src/Aspire.Hosting.Go/GoDelveServerAnnotation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ namespace Aspire.Hosting.Go;
/// </summary>
internal sealed class GoDelveServerAnnotation(
int port,
bool acceptMulticlient,
bool acceptMultiClient,
bool? onlySameUser,
bool continueOnStart,
bool log,
string logOutput) : IResourceAnnotation
string? logOutput) : IResourceAnnotation
{
public int Port { get; } = port;
public bool AcceptMulticlient { get; } = acceptMulticlient;
public bool AcceptMultiClient { get; } = acceptMultiClient;
public bool? OnlySameUser { get; } = onlySameUser;
public bool ContinueOnStart { get; } = continueOnStart;
public bool Log { get; } = log;
public string LogOutput { get; } = logOutput;
public string? LogOutput { get; } = logOutput;
}
Loading
Loading