Skip to content

Commit

Permalink
Add support for CosmosDB vnext-preview emulator (#7048)
Browse files Browse the repository at this point in the history
* 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

* Add tests and fix WithDataVolume for the preview emulator.

Also fixing the EF CosmosDB code to specify the PartitionKey.

* Add wait for healthy in cosmos tests.

* Skip the new Cosmos emulator tests until the emulator is more stable.

* Disable VerifyWaitForOnCosmosDBEmulatorBlocksDependentResources for the preview emulator
  • Loading branch information
eerhardt authored Jan 14, 2025
1 parent d7b7807 commit 78837be
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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() };
Expand Down Expand Up @@ -69,6 +69,12 @@ public class Entry
public class TestCosmosContext(DbContextOptions<TestCosmosContext> options) : DbContext(options)
{
public DbSet<EntityFrameworkEntry> Entries { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<EntityFrameworkEntry>()
.HasPartitionKey(e => e.Id);
}
}

public class EntityFrameworkEntry
Expand Down
4 changes: 3 additions & 1 deletion playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Projects.CosmosEndToEnd_ApiService>("api")
.WithExternalHttpEndpoints()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;");
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,8 @@ namespace Aspire.Hosting.Azure;
/// <param name="innerResource">The inner resource used to store annotations.</param>
public class AzureCosmosDBEmulatorResource(AzureCosmosDBResource innerResource) : ContainerResource(innerResource.Name), IResource
{
private readonly AzureCosmosDBResource _innerResource = innerResource;

/// <inheritdoc/>
public override string Name => _innerResource.Name;
internal AzureCosmosDBResource InnerResource { get; } = innerResource;

/// <inheritdoc />
public override ResourceAnnotationCollection Annotations => _innerResource.Annotations;
public override ResourceAnnotationCollection Annotations => InnerResource.Annotations;
}
63 changes: 58 additions & 5 deletions src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,7 +14,6 @@
using Azure.Provisioning.KeyVault;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.DependencyInjection;
using System.Globalization;

namespace Aspire.Hosting;

Expand Down Expand Up @@ -108,18 +109,36 @@ public static IResourceBuilder<AzureCosmosDBResource> AddAzureCosmosDB(this IDis
/// This version of the package defaults to the <inheritdoc cref="CosmosDBEmulatorContainerImageTags.Tag"/> tag of the <inheritdoc cref="CosmosDBEmulatorContainerImageTags.Registry"/>/<inheritdoc cref="CosmosDBEmulatorContainerImageTags.Image"/> container image.
/// </remarks>
public static IResourceBuilder<AzureCosmosDBResource> RunAsEmulator(this IResourceBuilder<AzureCosmosDBResource> builder, Action<IResourceBuilder<AzureCosmosDBEmulatorResource>>? configureContainer = null)
=> builder.RunAsEmulator(configureContainer, useVNextPreview: false);

/// <summary>
/// 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 <see cref="AzureCosmosDBResource"/> to be added to the application model.
/// For more information on the Azure Cosmos DB emulator, see <a href="https://learn.microsoft.com/azure/cosmos-db/emulator-linux"></a>.
/// </summary>
/// <param name="builder">The Azure Cosmos DB resource builder.</param>
/// <param name="configureContainer">Callback that exposes underlying container used for emulation to allow for customization.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
/// <remarks>
/// This version of the package defaults to the <inheritdoc cref="CosmosDBEmulatorContainerImageTags.TagVNextPreview"/> tag of the <inheritdoc cref="CosmosDBEmulatorContainerImageTags.Registry"/>/<inheritdoc cref="CosmosDBEmulatorContainerImageTags.Image"/> container image.
/// </remarks>
[Experimental("ASPIRECOSMOS001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")]
public static IResourceBuilder<AzureCosmosDBResource> RunAsPreviewEmulator(this IResourceBuilder<AzureCosmosDBResource> builder, Action<IResourceBuilder<AzureCosmosDBEmulatorResource>>? configureContainer = null)
=> builder.RunAsEmulator(configureContainer, useVNextPreview: true);

private static IResourceBuilder<AzureCosmosDBResource> RunAsEmulator(this IResourceBuilder<AzureCosmosDBResource> builder, Action<IResourceBuilder<AzureCosmosDBEmulatorResource>>? 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;
Expand Down Expand Up @@ -182,8 +201,12 @@ static CosmosClient CreateCosmosClient(string connectionString)
/// <param name="name">The name of the volume. Defaults to an auto-generated name based on the application and resource names.</param>
/// <returns>A builder for the <see cref="AzureCosmosDBEmulatorResource"/>.</returns>
public static IResourceBuilder<AzureCosmosDBEmulatorResource> WithDataVolume(this IResourceBuilder<AzureCosmosDBEmulatorResource> builder, string? name = null)
=> builder.WithEnvironment("AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE", "true")
.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/tmp/cosmos/appdata", isReadOnly: false);
{
var dataPath = builder.Resource.InnerResource.IsPreviewEmulator ? "/data": "/tmp/cosmos/appdata";

return builder.WithEnvironment("AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE", "true")
.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), dataPath, isReadOnly: false);
}

/// <summary>
/// Configures the gateway port for the Azure Cosmos DB emulator.
Expand All @@ -210,6 +233,11 @@ public static IResourceBuilder<AzureCosmosDBEmulatorResource> WithGatewayPort(th
/// </remarks>
public static IResourceBuilder<AzureCosmosDBEmulatorResource> WithPartitionCount(this IResourceBuilder<AzureCosmosDBEmulatorResource> 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.");
Expand All @@ -229,4 +257,29 @@ public static IResourceBuilder<AzureCosmosDBResource> AddDatabase(this IResource
builder.Resource.Databases.Add(databaseName);
return builder;
}

/// <summary>
/// Configures the Azure Cosmos DB preview emulator to expose the Data Explorer endpoint.
/// </summary>
/// <param name="builder">Builder for the Cosmos emulator container</param>
/// <param name="port">Optional host port to bind the Data Explorer to.</param>
/// <returns>Cosmos emulator resource builder.</returns>
/// <remarks>
/// The Data Explorer is only available with <see cref="RunAsPreviewEmulator"/>.
/// </remarks>
[Experimental("ASPIRECOSMOS001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")]
public static IResourceBuilder<AzureCosmosDBEmulatorResource> WithDataExplorer(this IResourceBuilder<AzureCosmosDBEmulatorResource> 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;
});
}
}
7 changes: 6 additions & 1 deletion src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure;
using Aspire.Hosting.Azure.Cosmos;

namespace Aspire.Hosting;

Expand All @@ -28,12 +29,16 @@ public class AzureCosmosDBResource(string name, Action<AzureResourceInfrastructu
/// </summary>
public bool IsEmulator => this.IsContainer();

internal bool IsPreviewEmulator =>
this.TryGetContainerImageName(out var imageName) &&
imageName == $"{CosmosDBEmulatorContainerImageTags.Registry}/{CosmosDBEmulatorContainerImageTags.Image}:{CosmosDBEmulatorContainerImageTags.TagVNextPreview}";

/// <summary>
/// Gets the connection string template for the manifest for the Azure Cosmos DB resource.
/// </summary>
public ReferenceExpression ConnectionStringExpression =>
IsEmulator
? AzureCosmosDBEmulatorConnectionString.Create(EmulatorEndpoint)
? AzureCosmosDBEmulatorConnectionString.Create(EmulatorEndpoint, IsPreviewEmulator)
: ReferenceExpression.Create($"{ConnectionString}");
}

Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ internal static class CosmosDBEmulatorContainerImageTags

/// <remarks>latest</remarks>
public const string Tag = "latest";

/// <remarks>vnext-preview</remarks>
public const string TagVNextPreview = "vnext-preview";
}
1 change: 0 additions & 1 deletion src/Aspire.Hosting.Azure.CosmosDB/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Aspire.Hosting.AzureCosmosDBResource!>!
static Aspire.Hosting.AzureCosmosExtensions.AddAzureCosmosDB(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, System.Action<Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.AzureCosmosDBResource!>!, Aspire.Hosting.ResourceModuleConstruct!, Azure.Provisioning.CosmosDB.CosmosDBAccount!, System.Collections.Generic.IEnumerable<Azure.Provisioning.CosmosDB.CosmosDBSqlDatabase!>!>? configureResource) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.AzureCosmosDBResource!>!
static Aspire.Hosting.AzureCosmosExtensions.AddDatabase(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.AzureCosmosDBResource!>! builder, string! databaseName) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.AzureCosmosDBResource!>!
Expand Down
2 changes: 2 additions & 0 deletions src/Aspire.Hosting.Azure.CosmosDB/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
*REMOVED*static Aspire.Hosting.AzureCosmosExtensions.AddAzureCosmosDB(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, System.Action<Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.AzureCosmosDBResource!>!, Aspire.Hosting.ResourceModuleConstruct!, Azure.Provisioning.CosmosDB.CosmosDBAccount!, System.Collections.Generic.IEnumerable<Azure.Provisioning.CosmosDB.CosmosDBSqlDatabase!>!>? configureResource) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.AzureCosmosDBResource!>!
*REMOVED*Aspire.Hosting.AzureCosmosDBResource.AzureCosmosDBResource(string! name, System.Action<Aspire.Hosting.ResourceModuleConstruct!>! configureConstruct) -> void
Aspire.Hosting.AzureCosmosDBResource.AzureCosmosDBResource(string! name, System.Action<Aspire.Hosting.Azure.AzureResourceInfrastructure!>! configureInfrastructure) -> void
static Aspire.Hosting.AzureCosmosExtensions.RunAsPreviewEmulator(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.AzureCosmosDBResource!>! builder, System.Action<Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureCosmosDBEmulatorResource!>!>? configureContainer = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.AzureCosmosDBResource!>!
static Aspire.Hosting.AzureCosmosExtensions.WithDataExplorer(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureCosmosDBEmulatorResource!>! builder, int? port = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureCosmosDBEmulatorResource!>!
static Aspire.Hosting.AzureCosmosExtensions.WithDataVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureCosmosDBEmulatorResource!>! builder, string? name = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureCosmosDBEmulatorResource!>!
static Aspire.Hosting.AzureCosmosExtensions.WithPartitionCount(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureCosmosDBEmulatorResource!>! builder, int count) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureCosmosDBEmulatorResource!>!
Loading

0 comments on commit 78837be

Please sign in to comment.