Skip to content

Commit

Permalink
Addressing port/endpoint issues on Qdrant (#3422)
Browse files Browse the repository at this point in the history
* Addressing port/endpoint issues on Qdrant

* PR feedback (param name)

* Fix up tests

* Respond to PR feedback.

Rename ports and endpoint names to be consistent.

* PR feedback
- moved to using grpc/http endpoint name consistent with config and prior art
- fixed wrong link in readme
- fixed/validated resource hosting test

---------

Co-authored-by: Eric Erhardt <[email protected]>
  • Loading branch information
timheuer and eerhardt authored Apr 5, 2024
1 parent e2b1b46 commit 374aade
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 34 deletions.
14 changes: 8 additions & 6 deletions src/Aspire.Hosting.Qdrant/QdrantBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,22 @@ public static class QdrantBuilderExtensions
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency</param>
/// <param name="apiKey">The parameter used to provide the API Key for the Qdrant resource. If <see langword="null"/> a random key will be generated as {name}-Key.</param>
/// <param name="port">The host port of Qdrant database.</param>
/// <param name="grpcPort">The host port of gRPC endpoint of Qdrant database.</param>
/// <param name="httpPort">The host port of HTTP endpoint of Qdrant database.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{QdrantServerResource}"/>.</returns>
public static IResourceBuilder<QdrantServerResource> AddQdrant(this IDistributedApplicationBuilder builder,
string name,
IResourceBuilder<ParameterResource>? apiKey = null,
int? port = null)
int? grpcPort = null,
int? httpPort = null)
{
var apiKeyParameter = apiKey?.Resource ??
ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-Key", special: false);
var qdrant = new QdrantServerResource(name, apiKeyParameter);
return builder.AddResource(qdrant)
.WithImage(QdrantContainerImageTags.Image, QdrantContainerImageTags.Tag)
.WithHttpEndpoint(port: port, targetPort: QdrantPortGrpc, name: QdrantServerResource.PrimaryEndpointName)
.WithHttpEndpoint(port: port, targetPort: QdrantPortHttp, name: QdrantServerResource.RestEndpointName)
.WithHttpEndpoint(port: grpcPort, targetPort: QdrantPortGrpc, name: QdrantServerResource.PrimaryEndpointName)
.WithHttpEndpoint(port: httpPort, targetPort: QdrantPortHttp, name: QdrantServerResource.HttpEndpointName)
.WithEnvironment(context =>
{
context.EnvironmentVariables[ApiKeyEnvVarName] = qdrant.ApiKeyParameter;
Expand Down Expand Up @@ -87,8 +89,8 @@ public static IResourceBuilder<IResourceWithEnvironment> WithReference(this IRes
// primary endpoint (gRPC)
context.EnvironmentVariables[$"ConnectionStrings__{qdrantResource.Resource.Name}"] = qdrantResource.Resource.ConnectionStringExpression;

// REST endpoint
context.EnvironmentVariables[$"ConnectionStrings__{qdrantResource.Resource.Name}_{QdrantServerResource.RestEndpointName}"] = qdrantResource.Resource.RestConnectionStringExpression;
// HTTP endpoint
context.EnvironmentVariables[$"ConnectionStrings__{qdrantResource.Resource.Name}_{QdrantServerResource.HttpEndpointName}"] = qdrantResource.Resource.HttpConnectionStringExpression;
});

return builder;
Expand Down
18 changes: 12 additions & 6 deletions src/Aspire.Hosting.Qdrant/QdrantServerResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ namespace Aspire.Hosting.ApplicationModel;
/// </summary>
public class QdrantServerResource : ContainerResource, IResourceWithConnectionString
{
internal const string PrimaryEndpointName = "http";
internal const string RestEndpointName = "rest";
internal const string PrimaryEndpointName = "grpc";
internal const string HttpEndpointName = "http";

/// <summary>
/// Initializes a new instance of the <see cref="QdrantServerResource"/> class.
Expand All @@ -23,17 +23,23 @@ public QdrantServerResource(string name, ParameterResource apiKey) : base(name)
}

private EndpointReference? _primaryEndpoint;
private EndpointReference? _httpEndpoint;

/// <summary>
/// Gets the parameter that contains the Qdrant API key.
/// </summary>
public ParameterResource ApiKeyParameter { get; }

/// <summary>
/// Gets the primary endpoint for the Qdrant database.
/// Gets the gRPC endpoint for the Qdrant database.
/// </summary>
public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName);

/// <summary>
/// Gets the HTTP endpoint for the Qdrant database.
/// </summary>
public EndpointReference HttpEndpoint => _httpEndpoint ??= new(this, HttpEndpointName);

/// <summary>
/// Gets the connection string expression for the Qdrant gRPC endpoint.
/// </summary>
Expand All @@ -42,9 +48,9 @@ public QdrantServerResource(string name, ParameterResource apiKey) : base(name)
$"Endpoint={PrimaryEndpoint.Property(EndpointProperty.Scheme)}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)};Key={ApiKeyParameter}");

/// <summary>
/// Gets the connection string expression for the Qdrant REST endpoint.
/// Gets the connection string expression for the Qdrant HTTP endpoint.
/// </summary>
public ReferenceExpression RestConnectionStringExpression =>
public ReferenceExpression HttpConnectionStringExpression =>
ReferenceExpression.Create(
$"Endpoint={PrimaryEndpoint.Property(EndpointProperty.Scheme)}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:6333;Key={ApiKeyParameter}");
$"Endpoint={HttpEndpoint.Property(EndpointProperty.Scheme)}://{HttpEndpoint.Property(EndpointProperty.Host)}:{HttpEndpoint.Property(EndpointProperty.Port)};Key={ApiKeyParameter}");
}
1 change: 0 additions & 1 deletion src/Aspire.Hosting.Qdrant/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ var myService = builder.AddProject<Projects.MyService>()

## Additional documentation
* https://qdrant.tech/documentation
* https://learn.microsoft.com/azure/cosmos-db/nosql/sdk-dotnet-v3

## Feedback & contributing

Expand Down
75 changes: 54 additions & 21 deletions tests/Aspire.Hosting.Tests/Qdrant/AddQdrantTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ namespace Aspire.Hosting.Tests.Qdrant;

public class AddQdrantTests
{
private const int QdrantPortHttp = 6334;
private const int QdrantPortDashboard = 6333;
private const int QdrantPortGrpc = 6334;
private const int QdrantPortHttp = 6333;

[Fact]
public async Task AddQdrantWithDefaultsAddsAnnotationMetadata()
Expand All @@ -34,11 +34,11 @@ public async Task AddQdrantWithDefaultsAddsAnnotationMetadata()
Assert.Null(containerAnnotation.Registry);

var endpoint = containerResource.Annotations.OfType<EndpointAnnotation>()
.FirstOrDefault(e => e.Name == "http");
.FirstOrDefault(e => e.Name == "grpc");
Assert.NotNull(endpoint);
Assert.Equal(QdrantPortHttp, endpoint.TargetPort);
Assert.Equal(QdrantPortGrpc, endpoint.TargetPort);
Assert.False(endpoint.IsExternal);
Assert.Equal("http", endpoint.Name);
Assert.Equal("grpc", endpoint.Name);
Assert.Null(endpoint.Port);
Assert.Equal(ProtocolType.Tcp, endpoint.Protocol);
Assert.Equal("http", endpoint.Transport);
Expand Down Expand Up @@ -73,12 +73,12 @@ public void AddQdrantWithDefaultsAndDashboardAddsAnnotationMetadata()
Assert.Null(containerAnnotation.Registry);

var endpoint = containerResource.Annotations.OfType<EndpointAnnotation>()
.FirstOrDefault(e => e.Name == "rest");
.FirstOrDefault(e => e.Name == "http");

Assert.NotNull(endpoint);
Assert.Equal(QdrantPortDashboard, endpoint.TargetPort);
Assert.Equal(QdrantPortHttp, endpoint.TargetPort);
Assert.False(endpoint.IsExternal);
Assert.Equal("rest", endpoint.Name);
Assert.Equal("http", endpoint.Name);
Assert.Null(endpoint.Port);
Assert.Equal(ProtocolType.Tcp, endpoint.Protocol);
Assert.Equal("http", endpoint.Transport);
Expand Down Expand Up @@ -107,11 +107,11 @@ public async Task AddQdrantAddsAnnotationMetadata()
Assert.Null(containerAnnotation.Registry);

var endpoint = containerResource.Annotations.OfType<EndpointAnnotation>()
.FirstOrDefault(e => e.Name == "http");
.FirstOrDefault(e => e.Name == "grpc");
Assert.NotNull(endpoint);
Assert.Equal(QdrantPortHttp, endpoint.TargetPort);
Assert.Equal(QdrantPortGrpc, endpoint.TargetPort);
Assert.False(endpoint.IsExternal);
Assert.Equal("http", endpoint.Name);
Assert.Equal("grpc", endpoint.Name);
Assert.Null(endpoint.Port);
Assert.Equal(ProtocolType.Tcp, endpoint.Protocol);
Assert.Equal("http", endpoint.Transport);
Expand All @@ -136,7 +136,7 @@ public async Task QdrantCreatesConnectionString()
var pass = appBuilder.AddParameter("pass");

var qdrant = appBuilder.AddQdrant("my-qdrant", pass)
.WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6334));
.WithEndpoint("grpc", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6334));

var connectionStringResource = qdrant.Resource as IResourceWithConnectionString;

Expand All @@ -154,8 +154,8 @@ public async Task QdrantClientAppWithReferenceContainsConnectionStrings()
var pass = appBuilder.AddParameter("pass");

var qdrant = appBuilder.AddQdrant("my-qdrant", pass)
.WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6334))
.WithEndpoint("rest", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6333));
.WithEndpoint("grpc", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6334))
.WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6333));

var projectA = appBuilder.AddProject<ProjectA>("projecta")
.WithReference(qdrant);
Expand All @@ -167,7 +167,7 @@ public async Task QdrantClientAppWithReferenceContainsConnectionStrings()
Assert.Equal(2, servicesKeysCount);

Assert.Contains(config, kvp => kvp.Key == "ConnectionStrings__my-qdrant" && kvp.Value == "Endpoint=http://localhost:6334;Key=pass");
Assert.Contains(config, kvp => kvp.Key == "ConnectionStrings__my-qdrant_rest" && kvp.Value == "Endpoint=http://localhost:6333;Key=pass");
Assert.Contains(config, kvp => kvp.Key == "ConnectionStrings__my-qdrant_http" && kvp.Value == "Endpoint=http://localhost:6333;Key=pass");
}

[Fact]
Expand All @@ -181,20 +181,20 @@ public async Task VerifyManifest()
var expectedManifest = $$"""
{
"type": "container.v0",
"connectionString": "Endpoint={qdrant.bindings.http.scheme}://{qdrant.bindings.http.host}:{qdrant.bindings.http.port};Key={qdrant-Key.value}",
"connectionString": "Endpoint={qdrant.bindings.grpc.scheme}://{qdrant.bindings.grpc.host}:{qdrant.bindings.grpc.port};Key={qdrant-Key.value}",
"image": "{{QdrantContainerImageTags.Image}}:{{QdrantContainerImageTags.Tag}}",
"env": {
"QDRANT__SERVICE__API_KEY": "{qdrant-Key.value}",
"QDRANT__SERVICE__ENABLE_STATIC_CONTENT": "0"
},
"bindings": {
"http": {
"grpc": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"targetPort": 6334
},
"rest": {
"http": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
Expand All @@ -219,20 +219,20 @@ public async Task VerifyManifestWithParameters()
var expectedManifest = $$"""
{
"type": "container.v0",
"connectionString": "Endpoint={qdrant.bindings.http.scheme}://{qdrant.bindings.http.host}:{qdrant.bindings.http.port};Key={QdrantApiKey.value}",
"connectionString": "Endpoint={qdrant.bindings.grpc.scheme}://{qdrant.bindings.grpc.host}:{qdrant.bindings.grpc.port};Key={QdrantApiKey.value}",
"image": "{{QdrantContainerImageTags.Image}}:{{QdrantContainerImageTags.Tag}}",
"env": {
"QDRANT__SERVICE__API_KEY": "{QdrantApiKey.value}",
"QDRANT__SERVICE__ENABLE_STATIC_CONTENT": "0"
},
"bindings": {
"http": {
"grpc": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"targetPort": 6334
},
"rest": {
"http": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
Expand All @@ -244,6 +244,39 @@ public async Task VerifyManifestWithParameters()
Assert.Equal(expectedManifest, serverManifest.ToString());
}

[Fact]
public void AddQdrantWithSpecifyingPorts()
{
using var builder = TestDistributedApplicationBuilder.Create();

var qdrant = builder.AddQdrant("my-qdrant", grpcPort: 5503, httpPort: 5504);

using var app = builder.Build();

var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

var qdrantResource = Assert.Single(appModel.Resources.OfType<QdrantServerResource>());
Assert.Equal("my-qdrant", qdrantResource.Name);

Assert.Equal(2, qdrantResource.Annotations.OfType<EndpointAnnotation>().Count());

var grpcEndpoint = qdrantResource.Annotations.OfType<EndpointAnnotation>().Single(e => e.Name == "grpc");
Assert.Equal(6334, grpcEndpoint.TargetPort);
Assert.False(grpcEndpoint.IsExternal);
Assert.Equal(5503, grpcEndpoint.Port);
Assert.Equal(ProtocolType.Tcp, grpcEndpoint.Protocol);
Assert.Equal("http", grpcEndpoint.Transport);
Assert.Equal("http", grpcEndpoint.UriScheme);

var httpEndpoint = qdrantResource.Annotations.OfType<EndpointAnnotation>().Single(e => e.Name == "http");
Assert.Equal(6333, httpEndpoint.TargetPort);
Assert.False(httpEndpoint.IsExternal);
Assert.Equal(5504, httpEndpoint.Port);
Assert.Equal(ProtocolType.Tcp, httpEndpoint.Protocol);
Assert.Equal("http", httpEndpoint.Transport);
Assert.Equal("http", httpEndpoint.UriScheme);
}

private static TestProgram CreateTestProgram(string[]? args = null) => TestProgram.Create<AddQdrantTests>(args);

private sealed class ProjectA : IProjectMetadata
Expand Down

0 comments on commit 374aade

Please sign in to comment.