// 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; #pragma warning disable ASPIREMONGODB001 namespace Aspire.Hosting.ApplicationModel; /// /// Represents a MongoDB replica set resource in the application model. /// A replica set is a group of MongoDB servers that maintain the same data set, providing redundancy and high availability. /// /// /// This resource is a logical grouping of multiple instances that are configured as members of the same replica set. /// /// The name of the resource, which is also the name of the replica set itself. /// The content of the keyfile the members use to authenticate to each other. Required. /// The user name every member authenticates with, or to use the default user name. /// The password every member authenticates with. Required. [Experimental("ASPIREMONGODB001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] [AspireExport(ExposeProperties = true)] public sealed class MongoDBReplicaSetResource( string name, ParameterResource keyFile, ParameterResource? sharedUserName, ParameterResource sharedPassword ) : Resource(name), IResourceWithWaitSupport, IResourceWithConnectionString { /// /// Gets the combined connection string for the MongoDB replica set, which includes the endpoints of all members, interpretable by the MongoDB driver. /// public ReferenceExpression ConnectionStringExpression => BuildConnectionString(); /// /// Gets the parameter that contains the content of the key file used for internal authentication between members of the MongoDB replica set. /// public ParameterResource SharedKeyFileParameter { get; } = keyFile ?? throw new ArgumentNullException(nameof(keyFile)); /// /// Gets the parameter that contains the username for authenticating to the MongoDB replica set. /// /// /// This will be the same across all members of the replica set, and is used in conjunction with for authentication. /// public ParameterResource? SharedUserNameParameter { get; } = sharedUserName; /// /// Gets the parameter that contains the password for authenticating to the MongoDB replica set. /// /// /// This will be the same across all members of the replica set, and is used in conjunction with for authentication. /// public ParameterResource SharedPasswordParameter { get; } = sharedPassword ?? throw new ArgumentNullException(nameof(sharedPassword)); /// /// Gets a reference to the username for the MongoDB replica set. /// public ReferenceExpression SharedUserNameReference => SharedUserNameParameter is null ? ReferenceExpression.Create($"{MongoDBServerResource.DefaultUserName}") : ReferenceExpression.Create($"{SharedUserNameParameter}"); /// /// The members that are currently stopped. The replica set has no process of its own for the orchestrator to follow, /// so its state is derived from the state of its members. /// internal HashSet StoppedMembers { get; } = new(StringComparers.ResourceName); /// /// Whether the replica set has been configured, meaning it can be reported as running again if its members come back. /// internal bool IsConfigured { get; set; } /// /// Gets the MongoDB server resources that are members of this replica set, in the order they were added. /// public IEnumerable Members => Annotations.OfType().Select(a => a.Member); private ReferenceExpression BuildConnectionString() { var membersList = Members.ToList(); if (membersList is []) { throw new InvalidOperationException($"Cannot build connection string for MongoDB replica set resource '{Name}' because it does not have any members."); } var builder = new ReferenceExpressionBuilder(); // Build the seed list `mongodb://host1:port1,host2:port2,.../?replicaSet=` — see https://www.mongodb.com/docs/manual/reference/connection-string/#dns-seedlist-connection-format builder.AppendLiteral("mongodb://"); if (SharedUserNameParameter is not null) { builder.Append($"{SharedUserNameParameter:uri}:{SharedPasswordParameter:uri}@"); } else { builder.Append($"{MongoDBServerResource.DefaultUserName:uri}:{SharedPasswordParameter:uri}@"); } for (var i = 0; i < membersList.Count; i++) { var member = membersList[i]; builder.Append($"{member.PrimaryEndpoint.Property(EndpointProperty.HostAndPort)}"); if (i < membersList.Count - 1) { builder.AppendLiteral(","); } } builder.AppendLiteral($"/?replicaSet={Name}"); builder.AppendLiteral("&authSource="); builder.Append($"{MongoDBServerResource.DefaultAuthenticationDatabase:uri}"); builder.AppendLiteral("&authMechanism="); builder.Append($"{MongoDBServerResource.DefaultAuthenticationMechanism:uri}"); // NOTE: TLS is turned on lazily (at `BeforeStartEvent` time, once a certificate is known to be available), so the // flag has to be resolved lazily here too. All members of a replica set share the same TLS configuration, so the // first member's endpoint is representative of the set. builder.Append($"{membersList[0].PrimaryEndpoint.GetTlsValue( enabledValue: ReferenceExpression.Create($"&tls=true"), disabledValue: ReferenceExpression.Empty)}"); return builder.Build(); } IEnumerable> IResourceWithConnectionString.GetConnectionProperties() { // NOTE: Unlike a single MongoDB server, a replica set has no one host and port to expose: clients are expected to // discover the members through the seed list in the connection string, so only the credentials, the replica set // name and the full URI are exposed here. yield return new("Username", SharedUserNameReference); yield return new("Password", ReferenceExpression.Create($"{SharedPasswordParameter}")); yield return new("AuthenticationDatabase", ReferenceExpression.Create($"{MongoDBServerResource.DefaultAuthenticationDatabase}")); yield return new("AuthenticationMechanism", ReferenceExpression.Create($"{MongoDBServerResource.DefaultAuthenticationMechanism}")); yield return new("ReplicaSetName", ReferenceExpression.Create($"{Name}")); yield return new("Uri", ConnectionStringExpression); } }