From 42cdc7bcbf517d1de4fc9d85e1ca4a7c36b88d01 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Tue, 19 Nov 2024 18:23:15 -0600 Subject: [PATCH] Add support for CosmosDB vnext-preview emulator Added a new experimental API - RunAsPreviewEmulator. This will use the new Linux-based emulator, which starts faster. And it also has support for a built-in Data Explorer which can be enabled by calling WithDataExplorer on the emulator. Fix #5163 --- .../CosmosEndToEnd.ApiService/Program.cs | 2 +- .../CosmosEndToEnd.AppHost/Program.cs | 4 +- .../AzureCosmosDBEmulatorConnectionString.cs | 5 +- .../AzureCosmosDBEmulatorResource.cs | 7 +-- .../AzureCosmosDBExtensions.cs | 54 +++++++++++++++++-- .../AzureCosmosDBResource.cs | 7 ++- .../CosmosDBEmulatorContainerImageTags.cs | 3 ++ .../PublicAPI.Shipped.txt | 1 - .../PublicAPI.Unshipped.txt | 2 + 9 files changed, 72 insertions(+), 13 deletions(-) diff --git a/playground/CosmosEndToEnd/CosmosEndToEnd.ApiService/Program.cs b/playground/CosmosEndToEnd/CosmosEndToEnd.ApiService/Program.cs index 7aec7379ffe..efca25cb079 100644 --- a/playground/CosmosEndToEnd/CosmosEndToEnd.ApiService/Program.cs +++ b/playground/CosmosEndToEnd/CosmosEndToEnd.ApiService/Program.cs @@ -20,7 +20,7 @@ app.MapGet("/", async (CosmosClient cosmosClient) => { var db = (await cosmosClient.CreateDatabaseIfNotExistsAsync("db")).Database; - var container = (await db.CreateContainerIfNotExistsAsync("entries", "/Id")).Container; + var container = (await db.CreateContainerIfNotExistsAsync("entries", "/id")).Container; // Add an entry to the database on each request. var newEntry = new Entry() { Id = Guid.NewGuid().ToString() }; diff --git a/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs b/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs index bf2cbff2c97..5e8cc180076 100644 --- a/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs +++ b/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs @@ -3,9 +3,11 @@ var builder = DistributedApplication.CreateBuilder(args); +#pragma warning disable ASPIRECOSMOS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var db = builder.AddAzureCosmosDB("cosmos") .AddDatabase("db") - .RunAsEmulator(); + .RunAsPreviewEmulator(e => e.WithDataExplorer()); +#pragma warning restore ASPIRECOSMOS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. builder.AddProject("api") .WithExternalHttpEndpoints() diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBEmulatorConnectionString.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBEmulatorConnectionString.cs index 9faee561a43..f8cd05afe85 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBEmulatorConnectionString.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBEmulatorConnectionString.cs @@ -8,5 +8,8 @@ namespace Aspire.Hosting.Azure; internal static class AzureCosmosDBEmulatorConnectionString { - public static ReferenceExpression Create(EndpointReference endpoint) => ReferenceExpression.Create($"AccountKey={CosmosConstants.EmulatorAccountKey};AccountEndpoint=https://{endpoint.Property(EndpointProperty.IPV4Host)}:{endpoint.Property(EndpointProperty.Port)};DisableServerCertificateValidation=True;"); + public static ReferenceExpression Create(EndpointReference endpoint, bool isPreviewEmulator) => + isPreviewEmulator + ? ReferenceExpression.Create($"AccountKey={CosmosConstants.EmulatorAccountKey};AccountEndpoint={endpoint.Property(EndpointProperty.Url)}") + : ReferenceExpression.Create($"AccountKey={CosmosConstants.EmulatorAccountKey};AccountEndpoint=https://{endpoint.Property(EndpointProperty.IPV4Host)}:{endpoint.Property(EndpointProperty.Port)};DisableServerCertificateValidation=True;"); } diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBEmulatorResource.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBEmulatorResource.cs index 1eacd90e279..d420fa3b45c 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBEmulatorResource.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBEmulatorResource.cs @@ -11,11 +11,8 @@ namespace Aspire.Hosting.Azure; /// The inner resource used to store annotations. public class AzureCosmosDBEmulatorResource(AzureCosmosDBResource innerResource) : ContainerResource(innerResource.Name), IResource { - private readonly AzureCosmosDBResource _innerResource = innerResource; - - /// - public override string Name => _innerResource.Name; + internal AzureCosmosDBResource InnerResource { get; } = innerResource; /// - public override ResourceAnnotationCollection Annotations => _innerResource.Annotations; + public override ResourceAnnotationCollection Annotations => InnerResource.Annotations; } diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs index dd73459e005..62f0ff992ed 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs @@ -1,6 +1,8 @@ // 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; +using System.Globalization; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; using Aspire.Hosting.Azure.Cosmos; @@ -12,7 +14,6 @@ using Azure.Provisioning.KeyVault; using Microsoft.Azure.Cosmos; using Microsoft.Extensions.DependencyInjection; -using System.Globalization; namespace Aspire.Hosting; @@ -108,18 +109,36 @@ public static IResourceBuilder AddAzureCosmosDB(this IDis /// This version of the package defaults to the tag of the / container image. /// public static IResourceBuilder RunAsEmulator(this IResourceBuilder builder, Action>? configureContainer = null) + => builder.RunAsEmulator(configureContainer, useVNextPreview: false); + + /// + /// Configures an Azure Cosmos DB resource to be emulated using the Azure Cosmos DB Linux-based emulator (preview) with the NoSQL API. This resource requires an to be added to the application model. + /// For more information on the Azure Cosmos DB emulator, see . + /// + /// The Azure Cosmos DB resource builder. + /// Callback that exposes underlying container used for emulation to allow for customization. + /// A reference to the . + /// + /// This version of the package defaults to the tag of the / container image. + /// + [Experimental("ASPIRECOSMOS001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")] + public static IResourceBuilder RunAsPreviewEmulator(this IResourceBuilder builder, Action>? configureContainer = null) + => builder.RunAsEmulator(configureContainer, useVNextPreview: true); + + private static IResourceBuilder RunAsEmulator(this IResourceBuilder builder, Action>? configureContainer, bool useVNextPreview) { if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode) { return builder; } - builder.WithEndpoint(name: "emulator", targetPort: 8081) + var scheme = useVNextPreview ? "http" : null; + builder.WithEndpoint(name: "emulator", scheme: scheme, targetPort: 8081) .WithAnnotation(new ContainerImageAnnotation { Registry = CosmosDBEmulatorContainerImageTags.Registry, Image = CosmosDBEmulatorContainerImageTags.Image, - Tag = CosmosDBEmulatorContainerImageTags.Tag + Tag = useVNextPreview ? CosmosDBEmulatorContainerImageTags.TagVNextPreview : CosmosDBEmulatorContainerImageTags.Tag }); CosmosClient? cosmosClient = null; @@ -210,6 +229,11 @@ public static IResourceBuilder WithGatewayPort(th /// public static IResourceBuilder WithPartitionCount(this IResourceBuilder builder, int count) { + if (builder.Resource.InnerResource.IsPreviewEmulator) + { + throw new NotSupportedException($"'{nameof(WithPartitionCount)}' does not work when using the preview version of the Azure Cosmos DB emulator."); + } + if (count < 1 || count > 250) { throw new ArgumentOutOfRangeException(nameof(count), count, "Count must be between 1 and 250."); @@ -229,4 +253,28 @@ public static IResourceBuilder AddDatabase(this IResource builder.Resource.Databases.Add(databaseName); return builder; } + + /// + /// Configures the Azure Cosmos DB preview emulator to expose the Data Explorer endpoint. + /// + /// Builder for the Cosmos emulator container + /// Optional host port to bind the Data Explorer to. + /// Cosmos emulator resource builder. + /// + /// The Data Explorer is only available with . + /// + public static IResourceBuilder WithDataExplorer(this IResourceBuilder builder, int? port = null) + { + if (!builder.Resource.InnerResource.IsPreviewEmulator) + { + throw new NotSupportedException($"The Data Explorer endpoint is only available when using the preview version of the Azure Cosmos DB emulator. Call '{nameof(RunAsPreviewEmulator)}' instead."); + } + + return builder.WithEndpoint(endpointName: "data-explorer", endpoint => + { + endpoint.UriScheme = "http"; + endpoint.TargetPort = 1234; + endpoint.Port = port; + }); + } } diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs index e829476fc46..94b1691a0e3 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs @@ -3,6 +3,7 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; +using Aspire.Hosting.Azure.Cosmos; namespace Aspire.Hosting; @@ -28,12 +29,16 @@ public class AzureCosmosDBResource(string name, Action public bool IsEmulator => this.IsContainer(); + internal bool IsPreviewEmulator => + this.TryGetContainerImageName(out var imageName) && + imageName == $"{CosmosDBEmulatorContainerImageTags.Registry}/{CosmosDBEmulatorContainerImageTags.Image}:{CosmosDBEmulatorContainerImageTags.TagVNextPreview}"; + /// /// Gets the connection string template for the manifest for the Azure Cosmos DB resource. /// public ReferenceExpression ConnectionStringExpression => IsEmulator - ? AzureCosmosDBEmulatorConnectionString.Create(EmulatorEndpoint) + ? AzureCosmosDBEmulatorConnectionString.Create(EmulatorEndpoint, IsPreviewEmulator) : ReferenceExpression.Create($"{ConnectionString}"); } diff --git a/src/Aspire.Hosting.Azure.CosmosDB/CosmosDBEmulatorContainerImageTags.cs b/src/Aspire.Hosting.Azure.CosmosDB/CosmosDBEmulatorContainerImageTags.cs index 76be2a184c1..7ca91640b66 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/CosmosDBEmulatorContainerImageTags.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/CosmosDBEmulatorContainerImageTags.cs @@ -13,4 +13,7 @@ internal static class CosmosDBEmulatorContainerImageTags /// latest public const string Tag = "latest"; + + /// vnext-preview + public const string TagVNextPreview = "vnext-preview"; } diff --git a/src/Aspire.Hosting.Azure.CosmosDB/PublicAPI.Shipped.txt b/src/Aspire.Hosting.Azure.CosmosDB/PublicAPI.Shipped.txt index 53ac0afce60..949dee6263b 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/PublicAPI.Shipped.txt +++ b/src/Aspire.Hosting.Azure.CosmosDB/PublicAPI.Shipped.txt @@ -8,7 +8,6 @@ Aspire.Hosting.AzureCosmosDBResource.ConnectionStringExpression.get -> Aspire.Ho Aspire.Hosting.AzureCosmosDBResource.IsEmulator.get -> bool Aspire.Hosting.AzureCosmosExtensions override Aspire.Hosting.Azure.AzureCosmosDBEmulatorResource.Annotations.get -> Aspire.Hosting.ApplicationModel.ResourceAnnotationCollection! -override Aspire.Hosting.Azure.AzureCosmosDBEmulatorResource.Name.get -> string! static Aspire.Hosting.AzureCosmosExtensions.AddAzureCosmosDB(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.AzureCosmosExtensions.AddAzureCosmosDB(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, System.Action!, Aspire.Hosting.ResourceModuleConstruct!, Azure.Provisioning.CosmosDB.CosmosDBAccount!, System.Collections.Generic.IEnumerable!>? configureResource) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.AzureCosmosExtensions.AddDatabase(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! databaseName) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! diff --git a/src/Aspire.Hosting.Azure.CosmosDB/PublicAPI.Unshipped.txt b/src/Aspire.Hosting.Azure.CosmosDB/PublicAPI.Unshipped.txt index d72fe00b8d4..a830d73815d 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/PublicAPI.Unshipped.txt +++ b/src/Aspire.Hosting.Azure.CosmosDB/PublicAPI.Unshipped.txt @@ -2,5 +2,7 @@ *REMOVED*static Aspire.Hosting.AzureCosmosExtensions.AddAzureCosmosDB(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, System.Action!, Aspire.Hosting.ResourceModuleConstruct!, Azure.Provisioning.CosmosDB.CosmosDBAccount!, System.Collections.Generic.IEnumerable!>? configureResource) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! *REMOVED*Aspire.Hosting.AzureCosmosDBResource.AzureCosmosDBResource(string! name, System.Action! configureConstruct) -> void Aspire.Hosting.AzureCosmosDBResource.AzureCosmosDBResource(string! name, System.Action! configureInfrastructure) -> void +static Aspire.Hosting.AzureCosmosExtensions.RunAsPreviewEmulator(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, System.Action!>? configureContainer = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.AzureCosmosExtensions.WithDataExplorer(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, int? port = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.AzureCosmosExtensions.WithDataVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.AzureCosmosExtensions.WithPartitionCount(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, int count) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!