Skip to content

Commit 53ffa9e

Browse files
committed
Add support for couchbase-index-manager via an add'l package
1 parent 96cdbfb commit 53ffa9e

16 files changed

+264
-27
lines changed

.github/workflows/cleanup-packages.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ jobs:
1919
package-type: nuget
2020
package-name: Couchbase.Aspire.Hosting
2121
min-versions-to-keep: 100
22+
- uses: actions/delete-package-versions@v5
23+
with:
24+
package-type: nuget
25+
package-name: Couchbase.Aspire.Hosting.Indices
26+
min-versions-to-keep: 100
2227
- uses: actions/delete-package-versions@v5
2328
with:
2429
package-type: nuget

couchbase-aspire.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<File Path="Directory.Packages.props" />
55
</Folder>
66
<Project Path="src/Couchbase.Aspire.Client/Couchbase.Aspire.Client.csproj" />
7+
<Project Path="src/Couchbase.Aspire.Hosting.Indices/Couchbase.Aspire.Hosting.Indices.csproj" />
78
<Project Path="src/Couchbase.Aspire.Hosting/Couchbase.Aspire.Hosting.csproj" />
89
<Project Path="src/Couchbase.HealthChecks/Couchbase.HealthChecks.csproj" />
910
<Project Path="tests/Aspire.Test.AppHost/Aspire.Test.AppHost.csproj" />
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
5+
<IsPackable>true</IsPackable>
6+
<PackageTags>aspire integration hosting Couchbase database data indexes indices</PackageTags>
7+
<Description>Couchbase Index Manager support for Aspire.</Description>
8+
<RootNamespace>Couchbase.Aspire.Hosting</RootNamespace>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<ProjectReference Include="..\Couchbase.Aspire.Hosting\Couchbase.Aspire.Hosting.csproj" />
13+
</ItemGroup>
14+
15+
</Project>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Couchbase.Aspire.Hosting;
2+
3+
internal static class CouchbaseIndexManagerImageTags
4+
{
5+
/// <remarks>docker.io</remarks>
6+
public const string Registry = "docker.io";
7+
8+
/// <remarks>btburnett3/couchbase-index-manager</remarks>
9+
public const string Image = "btburnett3/couchbase-index-manager";
10+
11+
/// <remarks>2.2.0</remarks>
12+
public const string Tag = "2.2.0";
13+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Aspire.Hosting.ApplicationModel;
2+
3+
namespace Couchbase.Aspire.Hosting;
4+
5+
/// <summary>
6+
/// Represents a Couchbase bucket resource within a Couchbase cluster, providing access to connection
7+
/// information and bucket metadata for integration and testing scenarios.
8+
/// </summary>
9+
/// <param name="name">The unique name of the resource instance.</param>
10+
/// <param name="bucket">The parent Couchbase bucket resource.</param>
11+
public class CouchbaseIndexManagerResource(string name, CouchbaseBucketResource bucket)
12+
: ContainerResource(name)
13+
{
14+
/// <summary>
15+
/// Gets the parent Couchbase bucket resource.
16+
/// </summary>
17+
public CouchbaseBucketResource Bucket { get; } = bucket ?? throw new ArgumentNullException(nameof(bucket));
18+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
using Aspire.Hosting;
2+
using Aspire.Hosting.ApplicationModel;
3+
using Couchbase.Aspire.Hosting;
4+
using Couchbase.KeyValue;
5+
6+
namespace Couchbase.Aspire.Hosting;
7+
8+
/// <summary>
9+
/// Extensions for managing Couchbase bucket indices.
10+
/// </summary>
11+
public static class CouchbaseBucketBuilderExtensions
12+
{
13+
/// <summary>
14+
/// Adds an index manager to the Couchbase bucket.
15+
/// </summary>
16+
/// <param name="builder">The bucket builder.</param>
17+
/// <param name="name">The name of the index manager resource.</param>
18+
/// <returns>The Couchbase index manager builder.</returns>
19+
public static IResourceBuilder<CouchbaseIndexManagerResource> AddIndexManager(this IResourceBuilder<CouchbaseBucketResource> builder,
20+
[ResourceName] string name)
21+
{
22+
ArgumentNullException.ThrowIfNull(builder);
23+
24+
var indexManager = new CouchbaseIndexManagerResource(name, builder.Resource);
25+
26+
return builder.ApplicationBuilder.AddResource(indexManager)
27+
.WithParentRelationship(builder)
28+
.WithImage(CouchbaseIndexManagerImageTags.Image, CouchbaseIndexManagerImageTags.Tag)
29+
.WithImageRegistry(CouchbaseIndexManagerImageTags.Registry)
30+
.WithIconName("Index")
31+
.WithArgs(context =>
32+
{
33+
var cluster = builder.Resource.Cluster;
34+
35+
context.Args.Add("-c");
36+
context.Args.Add(cluster.UriExpression);
37+
context.Args.Add("-u");
38+
context.Args.Add(cluster.UserNameReference);
39+
context.Args.Add("-p");
40+
context.Args.Add(cluster.PasswordParameter);
41+
context.Args.Add("sync");
42+
context.Args.Add("-f");
43+
context.Args.Add(builder.Resource.BucketNameExpression);
44+
})
45+
.WaitFor(builder);
46+
}
47+
48+
/// <summary>
49+
/// Adds index definitions to the Couchbase index manager.
50+
/// </summary>
51+
/// <param name="builder">The Couchbase index manager builder.</param>
52+
/// <param name="paths">List of relative paths to the index definitions.</param>
53+
/// <returns>The <paramref name="builder"/>.</returns>
54+
public static IResourceBuilder<CouchbaseIndexManagerResource> WithIndices(this IResourceBuilder<CouchbaseIndexManagerResource> builder,
55+
params string[] paths)
56+
{
57+
ArgumentNullException.ThrowIfNull(builder);
58+
ArgumentNullException.ThrowIfNull(paths);
59+
60+
if (paths.Length == 0)
61+
{
62+
return builder;
63+
}
64+
65+
var bindCount = builder.Resource.TryGetAnnotationsOfType<ContainerMountAnnotation>(out var annotations)
66+
? annotations.Count()
67+
: 0;
68+
69+
var bindPaths = new List<string>();
70+
foreach (var path in paths)
71+
{
72+
try
73+
{
74+
var attr = File.GetAttributes(path);
75+
var bindPath = attr.HasFlag(FileAttributes.Directory)
76+
? $"/definitions/{bindCount}"
77+
: $"/definitions/{bindCount}/{Path.GetFileName(path)}";
78+
79+
builder.WithBindMount(path, bindPath, isReadOnly: true);
80+
bindPaths.Add(bindPath);
81+
bindCount++;
82+
}
83+
catch (IOException ex)
84+
{
85+
throw new DistributedApplicationException($"Index definition path '{path}' is invalid.", ex);
86+
}
87+
}
88+
89+
builder
90+
.WithArgs(context =>
91+
{
92+
foreach (var bindPath in bindPaths)
93+
{
94+
context.Args.Add(bindPath);
95+
}
96+
});
97+
98+
return builder;
99+
}
100+
101+
/// <summary>
102+
/// Adds an index manager to the Couchbase bucket.
103+
/// </summary>
104+
/// <param name="builder">The bucket builder.</param>
105+
/// <param name="paths">List of relative paths to the index definitions.</param>
106+
/// <returns>The <paramref name="builder"/>.</returns>
107+
public static IResourceBuilder<CouchbaseBucketResource> WithIndices(this IResourceBuilder<CouchbaseBucketResource> builder,
108+
params string[] paths)
109+
{
110+
ArgumentNullException.ThrowIfNull(builder);
111+
ArgumentNullException.ThrowIfNull(paths);
112+
113+
if (paths.Length > 0)
114+
{
115+
builder.AddIndexManager($"{builder.Resource.Name}-index-manager")
116+
.WithIndices(paths);
117+
}
118+
119+
return builder;
120+
}
121+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Couchbase.Aspire.Hosting.Indices library
2+
3+
Provides extension methods and resource definitions to add index management to a Couchbase cluster resource in an Aspire AppHost.
4+
5+
## Getting Started
6+
7+
### Install the package
8+
9+
```dotnetcli
10+
dotnet add package Couchbase.Aspire.Hosting.Indices
11+
```
12+
13+
## Usage example
14+
15+
In the _AppHost.cs_ file of your `AppHost`, add indices to a Couchbase bucket resource using the following methods:
16+
17+
```csharp
18+
var couchbase = builder.AddCouchbase("couchbase");
19+
var bucket = couchbase.AddBucket("mybucket")
20+
.WithIndices("../indices");
21+
22+
var myService = builder.AddProject<Projects.MyService>()
23+
.WithReference(bucket)
24+
.WaitFor(bucket);
25+
```
26+
27+
### Waiting for indices to be ready
28+
29+
It is also possible to wait for the indices to be applied before starting your application:
30+
31+
```csharp
32+
var couchbase = builder.AddCouchbase("couchbase");
33+
var bucket = couchbase.AddBucket("mybucket");
34+
35+
var bucketIndices = bucket.AddIndexManager("mybucket-indices")
36+
.WithIndices("../indices");
37+
38+
var myService = builder.AddProject<Projects.MyService>()
39+
.WithReference(bucket)
40+
.WaitFor(bucket)
41+
.WaitForCompletion(bucketIndices);
42+
```
43+
44+
## Index definition files
45+
46+
Index definition files are JSON or YAML files that define the indexes to be created on a Couchbase bucket. They may be referenced by directory
47+
or by individual file. See [couchbase-index-manager Documentation](https://github.com/brantburnett/couchbase-index-manager/tree/main/packages/couchbase-index-manager-cli#definition-files) for details on the file format.
48+
49+
## Feedback & contributing
50+
51+
https://github.com/couchbaselabs/couchbase-aspire

src/Couchbase.Aspire.Hosting/Couchbase.Aspire.Hosting.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
<PropertyGroup>
44
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
5+
<IsPackable>true</IsPackable>
6+
<PackageTags>aspire integration hosting Couchbase database data</PackageTags>
7+
<Description>Couchbase support for Aspire.</Description>
58
</PropertyGroup>
69

710
<ItemGroup>

src/Couchbase.Aspire.Hosting/CouchbaseBucketBaseResource.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ internal interface ICouchbaseBucketResource<TSelf> where TSelf : ICouchbaseBucke
1616
/// </summary>
1717
/// <param name="name">The unique name of the resource instance.</param>
1818
/// <param name="bucketName">The name of the Couchbase bucket represented by this resource.</param>
19-
/// <param name="parent">The parent Couchbase cluster resource that hosts this bucket.</param>
20-
public abstract class CouchbaseBucketBaseResource(string name, string bucketName, CouchbaseClusterResource parent)
19+
/// <param name="cluster">The parent Couchbase cluster resource that hosts this bucket.</param>
20+
public abstract class CouchbaseBucketBaseResource(string name, string bucketName, CouchbaseClusterResource cluster)
2121
: Resource(name), IResourceWithWaitSupport, IResourceWithConnectionString, ICouchbaseCustomResource
2222
{
2323
/// <summary>
2424
/// Gets the parent Couchbase Server container resource.
2525
/// </summary>
26-
public CouchbaseClusterResource Parent { get; } = parent ?? throw new ArgumentNullException(nameof(parent));
26+
public CouchbaseClusterResource Cluster { get; } = cluster ?? throw new ArgumentNullException(nameof(cluster));
2727

2828
/// <summary>
2929
/// Gets the database name.
@@ -41,10 +41,10 @@ public abstract class CouchbaseBucketBaseResource(string name, string bucketName
4141
/// <remarks>
4242
/// Format: <c>couchbase://{user}:{password}@{host}:{port}/{bucketName}</c>.
4343
/// </remarks>
44-
public ReferenceExpression ConnectionStringExpression => Parent.BuildConnectionString(BucketName);
44+
public ReferenceExpression ConnectionStringExpression => Cluster.BuildConnectionString(BucketName);
4545

4646
IEnumerable<KeyValuePair<string, ReferenceExpression>> IResourceWithConnectionString.GetConnectionProperties() =>
47-
Parent.CombineProperties([
47+
Cluster.CombineProperties([
4848
new("BucketName", BucketNameExpression)
4949
]);
5050

src/Couchbase.Aspire.Hosting/CouchbaseBucketBuilderExtensions.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,13 @@ private static IResourceBuilder<T> AddBucket<T>(this IResourceBuilder<CouchbaseC
6262
builder.Resource.AddBucket(name, bucket);
6363

6464
string? connectionString = null;
65-
builder.ApplicationBuilder.Eventing.Subscribe<ConnectionStringAvailableEvent>(bucket.Parent, async (@event, ct) =>
65+
builder.ApplicationBuilder.Eventing.Subscribe<ConnectionStringAvailableEvent>(bucket.Cluster, async (@event, ct) =>
6666
{
67-
connectionString = await bucket.Parent.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
67+
connectionString = await bucket.Cluster.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
6868

6969
if (connectionString is null)
7070
{
71-
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{bucket.Parent.Name}' resource but the connection string was null.");
71+
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{bucket.Cluster.Name}' resource but the connection string was null.");
7272
}
7373
});
7474

@@ -79,7 +79,7 @@ private static IResourceBuilder<T> AddBucket<T>(this IResourceBuilder<CouchbaseC
7979
var options = new ClusterOptions()
8080
.WithConnectionString(connectionString ?? throw new InvalidOperationException("Connection string is unavailable"));
8181

82-
var cluster = bucket.Parent;
82+
var cluster = bucket.Cluster;
8383
options.UserName = await cluster.UserNameReference.GetValueAsync(ct).ConfigureAwait(false);
8484
options.Password = await cluster.PasswordParameter.GetValueAsync(ct).ConfigureAwait(false);
8585

@@ -98,7 +98,7 @@ private static IResourceBuilder<T> AddBucket<T>(this IResourceBuilder<CouchbaseC
9898
return await Cluster.ConnectAsync(options).WaitAsync(ct).ConfigureAwait(false);
9999
},
100100
bucketNameFactory: _ => bucket.BucketName,
101-
serviceRequirementsFactory: _ => bucket.Parent.GetHealthCheckServiceRequirements(),
101+
serviceRequirementsFactory: _ => bucket.Cluster.GetHealthCheckServiceRequirements(),
102102
name: healthCheckKey);
103103

104104
return builder.ApplicationBuilder
@@ -222,9 +222,9 @@ public static IResourceBuilder<CouchbaseBucketResource> WithFlushEnabled(this IR
222222
async (context) =>
223223
{
224224
var apiService = context.ServiceProvider.GetRequiredService<ICouchbaseApiService>();
225-
var api = apiService.GetApi(builder.Resource.Parent);
225+
var api = apiService.GetApi(builder.Resource.Cluster);
226226

227-
var server = builder.Resource.Parent.GetPrimaryServer();
227+
var server = builder.Resource.Cluster.GetPrimaryServer();
228228
if (server is null)
229229
{
230230
return new ExecuteCommandResult

0 commit comments

Comments
 (0)