77
88namespace Elastic . Transport ;
99
10+ /// <summary>
11+ /// Specifies which Elastic Cloud service to target when using a Cloud ID.
12+ /// </summary>
13+ public enum CloudService
14+ {
15+ /// <summary> Target the Elasticsearch cluster (default). </summary>
16+ Elasticsearch ,
17+ /// <summary> Target the Kibana instance. </summary>
18+ Kibana ,
19+ }
20+
1021/// <summary>
1122/// An <see cref="NodePool"/> implementation that can be seeded with a cloud id
1223/// and will signal the right defaults for the client to use for Elastic Cloud to <see cref="ITransportConfiguration"/>.
@@ -16,7 +27,7 @@ namespace Elastic.Transport;
1627/// </summary>
1728public sealed class CloudNodePool : SingleNodePool
1829{
19- private readonly record struct ParsedCloudId ( string ClusterName , Uri Uri ) ;
30+ private readonly record struct ParsedCloudId ( string ClusterName , Uri ElasticsearchUri , Uri ? KibanaUri ) ;
2031
2132 /// <summary>
2233 /// An <see cref="NodePool"/> implementation that can be seeded with a cloud id
@@ -28,31 +39,52 @@ public sealed class CloudNodePool : SingleNodePool
2839 /// <param name="cloudId">
2940 /// The Cloud Id, this is available on your cluster's dashboard and is a string in the form of <code>cluster_name:base_64_encoded_string</code>
3041 /// <para>Base64 encoded string contains the following tokens in order separated by $:</para>
31- /// <para>* Host Name (mandatory)</para>
32- /// <para>* Elasticsearch UUID (mandatory)</para>
33- /// <para>* Kibana UUID</para>
42+ /// <para>* Host Name (mandatory, optionally with :port, defaults to 443 )</para>
43+ /// <para>* Elasticsearch UUID (mandatory, optionally with :port )</para>
44+ /// <para>* Kibana UUID (optionally with :port) </para>
3445 /// <para>* APM UUID</para>
3546 /// <para></para>
3647 /// <para> We then use these tokens to create the URI to your Elastic Cloud cluster!</para>
3748 /// <para></para>
3849 /// <para> Read more here: https://www.elastic.co/guide/en/cloud/current/ec-cloud-id.html</para>
3950 /// </param>
40- /// <param name="credentials"></param>
41- public CloudNodePool ( string cloudId , AuthorizationHeader credentials ) : this ( ParseCloudId ( cloudId ) ) =>
51+ /// <param name="credentials">The credentials to use for authentication.</param>
52+ public CloudNodePool ( string cloudId , AuthorizationHeader credentials )
53+ : this ( ParseCloudId ( cloudId ) , CloudService . Elasticsearch ) =>
54+ AuthenticationHeader = credentials ;
55+
56+ /// <inheritdoc cref="CloudNodePool(string, AuthorizationHeader)"/>
57+ /// <param name="cloudId"><inheritdoc cref="CloudNodePool(string, AuthorizationHeader)" path="/param[@name='cloudId']"/></param>
58+ /// <param name="credentials"><inheritdoc cref="CloudNodePool(string, AuthorizationHeader)" path="/param[@name='credentials']"/></param>
59+ /// <param name="service">Which cloud service to target. Defaults to <see cref="CloudService.Elasticsearch"/>.</param>
60+ public CloudNodePool ( string cloudId , AuthorizationHeader credentials , CloudService service )
61+ : this ( ParseCloudId ( cloudId ) , service ) =>
4262 AuthenticationHeader = credentials ;
4363
4464 /// <summary>
45- /// An <see cref="NodePool"/> implementation that can be seeded with a cloud enpoint
65+ /// An <see cref="NodePool"/> implementation that can be seeded with a cloud endpoint
4666 /// and will signal the right defaults for the client to use for Elastic Cloud to <see cref="ITransportConfiguration"/>.
4767 /// </summary>
4868 /// <param name="cloudEndpoint">Elastic Cloud endpoint</param>
4969 /// <param name="credentials">The credentials to use with cloud</param>
50- public CloudNodePool ( Uri cloudEndpoint , AuthorizationHeader credentials ) : this ( CreateCloudId ( cloudEndpoint ) ) =>
70+ public CloudNodePool ( Uri cloudEndpoint , AuthorizationHeader credentials ) : this ( CreateCloudId ( cloudEndpoint ) , CloudService . Elasticsearch ) =>
5171 AuthenticationHeader = credentials ;
5272
53- private CloudNodePool ( ParsedCloudId parsedCloudId ) : base ( parsedCloudId . Uri ) =>
73+ private CloudNodePool ( ParsedCloudId parsedCloudId , CloudService service ) : base ( ResolveUri ( parsedCloudId , service ) ) =>
5474 ClusterName = parsedCloudId . ClusterName ;
5575
76+ private static Uri ResolveUri ( ParsedCloudId parsed , CloudService service )
77+ {
78+ if ( service == CloudService . Kibana )
79+ {
80+ if ( parsed . KibanaUri is null )
81+ throw new ArgumentException ( "The cloud ID does not contain a Kibana UUID. Cannot target Kibana." ) ;
82+ return parsed . KibanaUri ;
83+ }
84+
85+ return parsed . ElasticsearchUri ;
86+ }
87+
5688 //TODO implement debugger display for NodePool implementations and display it there and its ToString()
5789 // ReSharper disable once UnusedAutoPropertyAccessor.Local
5890 private string ClusterName { get ; }
@@ -66,13 +98,20 @@ private static ParsedCloudId CreateCloudId(Uri uri)
6698 var moniker = $ "{ uri . Host } ${ Guid . NewGuid ( ) : N} ";
6799 var base64 = Convert . ToBase64String ( Encoding . UTF8 . GetBytes ( moniker ) ) ;
68100 var cloudId = $ "name:{ base64 } ";
69- return new ParsedCloudId ( cloudId , uri ) ;
70-
101+ return new ParsedCloudId ( cloudId , uri , null ) ;
71102 }
72103
73104 private static readonly char [ ] ColonSeparator = [ ':' ] ;
74105 private static readonly char [ ] DollarSeparator = [ '$' ] ;
75106
107+ private static ( string Id , string Port ) ExtractPortFromId ( string input , string defaultPort = "443" )
108+ {
109+ var colonIndex = input . IndexOf ( ':' ) ;
110+ if ( colonIndex < 0 )
111+ return ( input , defaultPort ) ;
112+ return ( input [ ..colonIndex ] , input [ ( colonIndex + 1 ) ..] ) ;
113+ }
114+
76115 private static ParsedCloudId ParseCloudId ( string cloudId )
77116 {
78117 const string exceptionSuffix = "should be a string in the form of cluster_name:base_64_data" ;
@@ -93,14 +132,33 @@ private static ParsedCloudId ParseCloudId(string cloudId)
93132 if ( parts . Length < 2 )
94133 throw new ArgumentException ( $ "Parameter { nameof ( cloudId ) } decoded base_64_data contains less then 2 tokens, { exceptionSuffix } ", nameof ( cloudId ) ) ;
95134
96- var domainName = parts [ 0 ] . Trim ( ) ;
97- if ( string . IsNullOrWhiteSpace ( domainName ) )
135+ var ( host , defaultPort ) = ExtractPortFromId ( parts [ 0 ] . Trim ( ) ) ;
136+ if ( string . IsNullOrWhiteSpace ( host ) )
98137 throw new ArgumentException ( $ "Parameter { nameof ( cloudId ) } decoded base_64_data contains no domain name, { exceptionSuffix } ", nameof ( cloudId ) ) ;
99138
100- var elasticsearchUuid = parts [ 1 ] . Trim ( ) ;
101- if ( string . IsNullOrWhiteSpace ( elasticsearchUuid ) )
139+ var ( esId , esPort ) = ExtractPortFromId ( parts [ 1 ] . Trim ( ) , defaultPort ) ;
140+ if ( string . IsNullOrWhiteSpace ( esId ) )
102141 throw new ArgumentException ( $ "Parameter { nameof ( cloudId ) } decoded base_64_data contains no elasticsearch UUID, { exceptionSuffix } ", nameof ( cloudId ) ) ;
103142
104- return new ParsedCloudId ( clusterName , new Uri ( $ "https://{ elasticsearchUuid } .{ domainName } ") ) ;
143+ var esUri = BuildServiceUri ( esId , host , esPort ) ;
144+
145+ Uri ? kibanaUri = null ;
146+ if ( parts . Length >= 3 )
147+ {
148+ var kibanaRaw = parts [ 2 ] . Trim ( ) ;
149+ if ( ! string . IsNullOrWhiteSpace ( kibanaRaw ) )
150+ {
151+ var ( kbId , kbPort ) = ExtractPortFromId ( kibanaRaw , defaultPort ) ;
152+ if ( ! string . IsNullOrWhiteSpace ( kbId ) )
153+ kibanaUri = BuildServiceUri ( kbId , host , kbPort ) ;
154+ }
155+ }
156+
157+ return new ParsedCloudId ( clusterName , esUri , kibanaUri ) ;
105158 }
159+
160+ private static Uri BuildServiceUri ( string serviceId , string host , string port ) =>
161+ port == "443"
162+ ? new Uri ( $ "https://{ serviceId } .{ host } ")
163+ : new Uri ( $ "https://{ serviceId } .{ host } :{ port } ") ;
106164}
0 commit comments