File: AzureKustoClusterResource.cs
Web Access
Project: src\src\Aspire.Hosting.Azure.Kusto\Aspire.Hosting.Azure.Kusto.csproj (Aspire.Hosting.Azure.Kusto)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#pragma warning disable AZPROVISION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
 
using Aspire.Hosting.ApplicationModel;
using Azure.Provisioning;
using Azure.Provisioning.Authorization;
using Azure.Provisioning.Expressions;
using Azure.Provisioning.Kusto;
using Azure.Provisioning.Primitives;
 
namespace Aspire.Hosting.Azure;
 
/// <summary>
/// A resource that represents a Kusto cluster.
/// </summary>
public class AzureKustoClusterResource : AzureProvisioningResource, IResourceWithConnectionString, IResourceWithEndpoints
{
    /// <summary>
    /// Initializes a new instance of the <see cref="AzureKustoClusterResource"/> class.
    /// </summary>
    /// <param name="name">The name of the resource.</param>
    /// <param name="configureInfrastructure">Callback to configure the Azure resources.</param>
    public AzureKustoClusterResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure)
        : base(name, configureInfrastructure)
    {
    }
 
    /// <summary>
    /// Gets whether the resource is running the local emulator.
    /// </summary>
    public bool IsEmulator => this.IsContainer();
 
    /// <summary>
    /// Gets the "name" output reference for the resource.
    /// </summary>
    public BicepOutputReference NameOutputReference => new("name", this);
 
    /// <summary>
    /// Gets the cluster URI output reference for the Azure Kusto cluster.
    /// </summary>
    public BicepOutputReference ClusterUri => new("clusterUri", this);
 
    /// <inheritdoc/>
    public ReferenceExpression ConnectionStringExpression
    {
        get
        {
            // Check if this resource is marked as an emulator
            if (IsEmulator)
            {
                // For emulator, use the HTTP endpoint pattern
                var endpoint = this.GetEndpoint("http");
                return ReferenceExpression.Create($"{endpoint}");
            }
            else
            {
                // For Azure provisioned resources, use the cluster URI output
                return ReferenceExpression.Create($"{ClusterUri}");
            }
        }
    }
 
    /// <summary>
    /// The databases for this cluster.
    /// </summary>
    internal List<AzureKustoReadWriteDatabaseResource> Databases { get; } = [];
 
    /// <inheritdoc/>
    public override ProvisionableResource AddAsExistingResource(AzureResourceInfrastructure infra)
    {
        var bicepIdentifier = this.GetBicepIdentifier();
        var resources = infra.GetProvisionableResources();
 
        // Check if a KustoCluster with the same identifier already exists
        var existingCluster = resources.OfType<KustoCluster>().SingleOrDefault(cluster => cluster.BicepIdentifier == bicepIdentifier);
 
        if (existingCluster is not null)
        {
            return existingCluster;
        }
 
        // Create and add new resource if it doesn't exist
        var cluster = KustoCluster.FromExisting(bicepIdentifier);
 
        if (!TryApplyExistingResourceAnnotation(
            this,
            infra,
            cluster))
        {
            cluster.Name = NameOutputReference.AsProvisioningParameter(infra);
        }
 
        infra.Add(cluster);
        return cluster;
    }
 
    /// <inheritdoc/>
    public override void AddRoleAssignments(IAddRoleAssignmentsContext roleAssignmentContext)
    {
        var infra = roleAssignmentContext.Infrastructure;
        var kusto = (KustoCluster)AddAsExistingResource(infra);
 
        var principalId = roleAssignmentContext.PrincipalId;
        var principalType = roleAssignmentContext.PrincipalType;
 
        foreach (var db in Databases)
        {
            var kustoDb = KustoReadWriteDatabase.FromExisting(Infrastructure.NormalizeBicepIdentifier(db.Name));
            kustoDb.Parent = kusto;
            kustoDb.Name = db.DatabaseName;
            infra.Add(kustoDb);
 
            infra.Add(new KustoDatabasePrincipalAssignment($"{kustoDb.BicepIdentifier}_user")
            {
                Name = BicepFunction.CreateGuid(kustoDb.Id, principalId, "User"),
                Parent = kustoDb,
                DatabasePrincipalId = principalId,
                Role = KustoDatabasePrincipalRole.User,
                PrincipalType = GetKustoPrincipalType(principalType)
            });
        }
    }
 
    private static BicepValue<KustoPrincipalAssignmentType> GetKustoPrincipalType(BicepValue<RoleManagementPrincipalType> principalType)
    {
        IBicepValue principalTypeBicepValue = principalType;
        var kind = principalTypeBicepValue.Kind;
        if (kind == BicepValueKind.Expression)
        {
            return new ConditionalExpression(
                new BinaryExpression(
                    principalTypeBicepValue.Expression!,
                    BinaryBicepOperator.Equal,
                    new StringLiteralExpression("User")),
                new StringLiteralExpression("User"),
                new ConditionalExpression(
                    new BinaryExpression(
                        principalTypeBicepValue.Expression!,
                        BinaryBicepOperator.Equal,
                        new StringLiteralExpression("Group")),
                    new StringLiteralExpression("Group"),
                    new StringLiteralExpression("App") // Default to App if not User or Group
                )
            );
        }
        else if (kind == BicepValueKind.Literal)
        {
            return principalType.Value switch
            {
                RoleManagementPrincipalType.User => KustoPrincipalAssignmentType.User,
                RoleManagementPrincipalType.Group => KustoPrincipalAssignmentType.Group,
                RoleManagementPrincipalType.ServicePrincipal => KustoPrincipalAssignmentType.App,
                _ => throw new InvalidOperationException($"Unsupported principal type: {principalType}")
            };
        }
 
        throw new InvalidOperationException($"Unsupported principal type kind: {kind}");
    }
}