| File: AzureManagedRedisResource.cs | Web Access |
| Project: src\src\Aspire.Hosting.Azure.Redis\Aspire.Hosting.Azure.Redis.csproj (Aspire.Hosting.Azure.Redis) |
// 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 ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Aspire.Hosting.ApplicationModel; using Azure.Provisioning; using Azure.Provisioning.Expressions; using Azure.Provisioning.Primitives; using Azure.Provisioning.RedisEnterprise; namespace Aspire.Hosting.Azure; /// <summary> /// Represents an Azure Managed Redis resource. /// </summary> /// <param name="name">The name of the resource.</param> /// <param name="configureInfrastructure">Callback to configure the Azure resources.</param> [AspireExport(ExposeProperties = true)] public class AzureManagedRedisResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure) : AzureProvisioningResource(name, configureInfrastructure), IResourceWithConnectionString, IAzurePrivateEndpointTarget { /// <summary> /// Gets the "connectionString" output reference from the bicep template for the Azure Managed Redis resource. /// /// This is used when Entra ID authentication is used. The connection string is an output of the bicep template. /// </summary> private BicepOutputReference ConnectionStringOutput => new("connectionString", this); /// <summary> /// Gets the "connectionString" secret reference from the key vault associated with this resource. /// /// This is set when access key authentication is used. The connection string is stored in a secret in the Azure Key Vault. /// </summary> internal IAzureKeyVaultSecretReference? ConnectionStringSecretOutput { get; set; } /// <summary> /// Gets the "primaryAccessKey" secret reference from the key vault associated with this resource. /// /// This is set when access key authentication is used. The primary access key is stored in a secret in the Azure Key Vault. /// </summary> internal IAzureKeyVaultSecretReference? PrimaryAccessKeySecretOutput { get; set; } /// <summary> /// Gets the "name" output reference for the resource. /// </summary> /// <remarks>This property is not available in polyglot app hosts.</remarks> [AspireExportIgnore] public BicepOutputReference NameOutputReference => new("name", this); /// <summary> /// Gets the "id" output reference for the resource. /// </summary> /// <remarks>This property is not available in polyglot app hosts.</remarks> [AspireExportIgnore] public BicepOutputReference Id => new("id", this); /// <summary> /// Gets the "hostName" output reference from the bicep template for the Azure Redis resource. /// </summary> private BicepOutputReference HostNameOutput => new("hostName", this); /// <summary> /// Gets a value indicating whether the resource uses access key authentication. /// </summary> [MemberNotNullWhen(true, nameof(ConnectionStringSecretOutput))] [MemberNotNullWhen(true, nameof(PrimaryAccessKeySecretOutput))] public bool UseAccessKeyAuthentication => ConnectionStringSecretOutput is not null; /// <summary> /// Gets the inner Redis resource. /// /// This is set when RunAsContainer is called on the AzureManagedRedisResource resource to create a local Redis container. /// </summary> internal RedisResource? InnerResource { get; private set; } /// <inheritdoc /> /// <remarks>This property is not available in polyglot app hosts.</remarks> [AspireExportIgnore] public override ResourceAnnotationCollection Annotations => InnerResource?.Annotations ?? base.Annotations; /// <summary> /// Gets the connection string template for the manifest for the Azure Managed Redis resource. /// </summary> public ReferenceExpression ConnectionStringExpression => InnerResource?.ConnectionStringExpression ?? (UseAccessKeyAuthentication ? ReferenceExpression.Create($"{ConnectionStringSecretOutput}") : ReferenceExpression.Create($"{ConnectionStringOutput}")); /// <summary> /// Gets the host name for the Redis server. /// </summary> /// <remarks> /// In container mode, resolves to the container's primary endpoint host and port. /// In Azure mode, resolves to the Azure Redis server's hostname. /// </remarks> public ReferenceExpression HostName => InnerResource is not null ? ReferenceExpression.Create($"{InnerResource.PrimaryEndpoint.Property(EndpointProperty.Host)}") : ReferenceExpression.Create($"{HostNameOutput}"); /// <summary> /// Gets the port for the Redis server. /// </summary> /// <remarks> /// In container mode, resolves to the container's primary endpoint port. /// In Azure mode, resolves to 10000. /// </remarks> public ReferenceExpression Port => InnerResource is not null ? ReferenceExpression.Create($"{InnerResource.Port}") : ReferenceExpression.Create($"10000"); // Based on the ConfigureRedisInfrastructure method /// <summary> /// Gets the password/access key for the Redis server. /// </summary> /// <remarks> /// <list type="bullet"> /// <item>When running as a container, returns the password parameter value if one exists.</item> /// <item>When using access key authentication in Azure mode, returns the primary access key from KeyVault.</item> /// <item>When using Entra ID authentication in Azure mode, returns an empty expression.</item> /// </list> /// </remarks> public ReferenceExpression? Password => InnerResource is not null ? (InnerResource.PasswordParameter is not null ? ReferenceExpression.Create($"{InnerResource.PasswordParameter}") : null) : UseAccessKeyAuthentication ? ReferenceExpression.Create($"{PrimaryAccessKeySecretOutput}") : null; /// <summary> /// Gets the connection URI expression for the Redis server. /// </summary> /// <remarks> /// Format: <c>redis://[:{password}@]{host}:{port}</c>. The password segment is omitted when using Entra ID authentication in Azure mode. /// </remarks> public ReferenceExpression UriExpression { get { if (InnerResource is not null) { return InnerResource.UriExpression; } if (UseAccessKeyAuthentication) { return ReferenceExpression.Create($"rediss://:{PrimaryAccessKeySecretOutput:uri}@{HostName}:{Port}"); } return ReferenceExpression.Create($"rediss://{HostName}:{Port}"); } } internal void SetInnerResource(RedisResource innerResource) { // Copy the annotations to the inner resource before making it the inner resource foreach (var annotation in Annotations) { innerResource.Annotations.Add(annotation); } InnerResource = innerResource; } /// <inheritdoc/> public override ProvisionableResource AddAsExistingResource(AzureResourceInfrastructure infra) { var bicepIdentifier = this.GetBicepIdentifier(); var resources = infra.GetProvisionableResources(); // Check if a RedisEnterpriseCluster with the same identifier already exists var existingCluster = resources.OfType<RedisEnterpriseCluster>().SingleOrDefault(cluster => cluster.BicepIdentifier == bicepIdentifier); if (existingCluster is not null) { return existingCluster; } // Create and add new resource if it doesn't exist var cluster = RedisEnterpriseCluster.FromExisting(bicepIdentifier); if (!TryApplyExistingResourceAnnotation( this, infra, cluster)) { cluster.Name = NameOutputReference.AsProvisioningParameter(infra); } infra.Add(cluster); return cluster; } /// <inheritdoc/> public override void AddRoleAssignments(IAddRoleAssignmentsContext roleAssignmentContext) { Debug.Assert(!UseAccessKeyAuthentication, "AddRoleAssignments should not be called when using AccessKeyAuthentication"); var infra = roleAssignmentContext.Infrastructure; var redisEnterprise = (RedisEnterpriseCluster)AddAsExistingResource(infra); var redisEnterpriseDatabase = infra.GetProvisionableResources() .OfType<RedisEnterpriseDatabase>() .SingleOrDefault(db => db.BicepIdentifier == redisEnterprise.BicepIdentifier + "_default"); if (redisEnterpriseDatabase is null) { redisEnterpriseDatabase = RedisEnterpriseDatabase.FromExisting(redisEnterprise.BicepIdentifier + "_default"); redisEnterpriseDatabase.Name = "default"; redisEnterpriseDatabase.Parent = redisEnterprise; infra.Add(redisEnterpriseDatabase); } var principalId = roleAssignmentContext.PrincipalId; AddEnterpriseContributorPolicyAssignment(infra, redisEnterpriseDatabase, principalId); } private static void AddEnterpriseContributorPolicyAssignment(AzureResourceInfrastructure infra, RedisEnterpriseDatabase database, BicepValue<Guid> principalId) { // For Redis Enterprise, we need to use the appropriate access policy assignment // This may need to be adjusted based on the actual Azure.Provisioning.Redis types available infra.Add(new AccessPolicyAssignment($"{database.BicepIdentifier}_contributor") { Name = BicepFunction.CreateGuid(database.Id, principalId, "default"), Parent = database, AccessPolicyName = "default", UserObjectId = principalId }); } IEnumerable<KeyValuePair<string, ReferenceExpression>> IResourceWithConnectionString.GetConnectionProperties() { if (InnerResource is not null) { foreach (var property in ((IResourceWithConnectionString)InnerResource).GetConnectionProperties()) { yield return property; } yield break; } yield return new("Host", HostName); yield return new("Port", Port); yield return new("Uri", UriExpression); if (Password is not null) { yield return new("Password", Password); } } BicepOutputReference IAzurePrivateEndpointTarget.Id => Id; IEnumerable<string> IAzurePrivateEndpointTarget.GetPrivateLinkGroupIds() => ["redisEnterprise"]; string IAzurePrivateEndpointTarget.GetPrivateDnsZoneName() => "privatelink.redis.azure.net"; }