1+ namespace Testcontainers . OpenSearch ;
2+
3+ /// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
4+ [ PublicAPI ]
5+ public sealed class OpenSearchBuilder : ContainerBuilder < OpenSearchBuilder , OpenSearchContainer , OpenSearchConfiguration >
6+ {
7+ public const string OpenSearchImage = "opensearchproject/opensearch:2.12.0" ;
8+
9+ public const ushort OpenSearchRestApiPort = 9200 ;
10+
11+ public const ushort OpenSearchTransportPort = 9300 ;
12+
13+ public const ushort OpenSearchPerformanceAnalyzerPort = 9600 ;
14+
15+ public const string DefaultUsername = "admin" ;
16+
17+ public const string DefaultPassword = "yourStrong(!)P@ssw0rd" ;
18+
19+ /// <summary>
20+ /// Initializes a new instance of the <see cref="OpenSearchBuilder" /> class.
21+ /// </summary>
22+ public OpenSearchBuilder ( )
23+ : this ( new OpenSearchConfiguration ( ) )
24+ {
25+ DockerResourceConfiguration = Init ( ) . DockerResourceConfiguration ;
26+ }
27+
28+ /// <summary>
29+ /// Initializes a new instance of the <see cref="OpenSearchBuilder" /> class.
30+ /// </summary>
31+ /// <param name="resourceConfiguration">The Docker resource configuration.</param>
32+ private OpenSearchBuilder ( OpenSearchConfiguration resourceConfiguration )
33+ : base ( resourceConfiguration )
34+ {
35+ DockerResourceConfiguration = resourceConfiguration ;
36+ }
37+
38+ /// <inheritdoc />
39+ protected override OpenSearchConfiguration DockerResourceConfiguration { get ; }
40+
41+ /// <summary>
42+ /// Sets the password for the <c>admin</c> user.
43+ /// </summary>
44+ /// <remarks>
45+ /// The password must meet the following complexity requirements:
46+ /// <list type="bullet">
47+ /// <item><description>Minimum of 8 characters</description></item>
48+ /// <item><description>At least one uppercase letter</description></item>
49+ /// <item><description>At least one lowercase letter</description></item>
50+ /// <item><description>At least one digit</description></item>
51+ /// <item><description>At least one special character</description></item>
52+ /// </list>
53+ /// </remarks>
54+ /// <param name="password">The <c>admin</c> user password.</param>
55+ /// <returns>A configured instance of <see cref="OpenSearchBuilder" />.</returns>
56+ public OpenSearchBuilder WithPassword ( string password )
57+ {
58+ return Merge ( DockerResourceConfiguration , new OpenSearchConfiguration ( password : password ) )
59+ . WithEnvironment ( "OPENSEARCH_INITIAL_ADMIN_PASSWORD" , password ) ;
60+ }
61+
62+ /// <summary>
63+ /// Enables or disables the built-in security plugin in OpenSearch.
64+ /// </summary>
65+ /// <remarks>
66+ /// When disabled, the <see cref="OpenSearchContainer.GetConnectionString" /> method
67+ /// will use the <c>http</c> protocol instead of <c>https</c>.
68+ /// </remarks>
69+ /// <param name="securityEnabled"><c>true</c> to enable the security plugin; <c>false</c> to disable it.</param>
70+ /// <returns>A configured instance of <see cref="OpenSearchBuilder" />.</returns>
71+ public OpenSearchBuilder WithSecurityEnabled ( bool securityEnabled = true )
72+ {
73+ return Merge ( DockerResourceConfiguration , new OpenSearchConfiguration ( tlsEnabled : securityEnabled ) )
74+ . WithEnvironment ( "plugins.security.disabled" , ( ! securityEnabled ) . ToString ( ) . ToLowerInvariant ( ) ) ;
75+ }
76+
77+ /// <inheritdoc />
78+ public override OpenSearchContainer Build ( )
79+ {
80+ Validate ( ) ;
81+
82+ OpenSearchBuilder openSearchBuilder ;
83+
84+ Predicate < System . Version > predicate = v => v . Major == 1 || ( v . Major == 2 && v . Minor < 12 ) ;
85+
86+ var image = DockerResourceConfiguration . Image ;
87+
88+ // Images before version 2.12.0 use a hardcoded default password.
89+ var requiresHardcodedDefaultPassword = image . MatchVersion ( predicate ) ;
90+ if ( requiresHardcodedDefaultPassword )
91+ {
92+ openSearchBuilder = WithPassword ( "admin" ) ;
93+ }
94+ else
95+ {
96+ openSearchBuilder = this ;
97+ }
98+
99+ // By default, the base builder waits until the container is running. However, for OpenSearch, a more advanced waiting strategy is necessary that requires access to the password.
100+ // If the user does not provide a custom waiting strategy, append the default OpenSearch waiting strategy.
101+ openSearchBuilder = DockerResourceConfiguration . WaitStrategies . Count ( ) > 1 ? openSearchBuilder : openSearchBuilder . WithWaitStrategy ( Wait . ForUnixContainer ( ) . AddCustomWaitStrategy ( new WaitUntil ( DockerResourceConfiguration ) ) ) ;
102+ return new OpenSearchContainer ( openSearchBuilder . DockerResourceConfiguration ) ;
103+ }
104+
105+ /// <inheritdoc />
106+ protected override OpenSearchBuilder Init ( )
107+ {
108+ return base . Init ( )
109+ . WithImage ( OpenSearchImage )
110+ . WithPortBinding ( OpenSearchRestApiPort , true )
111+ . WithPortBinding ( OpenSearchTransportPort , true )
112+ . WithPortBinding ( OpenSearchPerformanceAnalyzerPort , true )
113+ . WithEnvironment ( "discovery.type" , "single-node" )
114+ . WithSecurityEnabled ( )
115+ . WithUsername ( DefaultUsername )
116+ . WithPassword ( DefaultPassword ) ;
117+ }
118+
119+ /// <inheritdoc />
120+ protected override void Validate ( )
121+ {
122+ base . Validate ( ) ;
123+
124+ _ = Guard . Argument ( DockerResourceConfiguration . Password , nameof ( DockerResourceConfiguration . Password ) )
125+ . NotNull ( )
126+ . NotEmpty ( ) ;
127+ }
128+
129+ /// <inheritdoc />
130+ protected override OpenSearchBuilder Clone ( IResourceConfiguration < CreateContainerParameters > resourceConfiguration )
131+ {
132+ return Merge ( DockerResourceConfiguration , new OpenSearchConfiguration ( resourceConfiguration ) ) ;
133+ }
134+
135+ /// <inheritdoc />
136+ protected override OpenSearchBuilder Clone ( IContainerConfiguration resourceConfiguration )
137+ {
138+ return Merge ( DockerResourceConfiguration , new OpenSearchConfiguration ( resourceConfiguration ) ) ;
139+ }
140+
141+ /// <inheritdoc />
142+ protected override OpenSearchBuilder Merge ( OpenSearchConfiguration oldValue , OpenSearchConfiguration newValue )
143+ {
144+ return new OpenSearchBuilder ( new OpenSearchConfiguration ( oldValue , newValue ) ) ;
145+ }
146+
147+ /// <summary>
148+ /// Sets the OpenSearch username.
149+ /// </summary>
150+ /// <remarks>
151+ /// The Docker image does not allow to configure the username.
152+ /// </remarks>
153+ /// <param name="username">The OpenSearch username.</param>
154+ /// <returns>A configured instance of <see cref="OpenSearchBuilder" />.</returns>
155+ private OpenSearchBuilder WithUsername ( string username )
156+ {
157+ return Merge ( DockerResourceConfiguration , new OpenSearchConfiguration ( username : username ) ) ;
158+ }
159+
160+ /// <inheritdoc cref="IWaitUntil" />
161+ private sealed class WaitUntil : IWaitUntil
162+ {
163+ private readonly bool _tlsEnabled ;
164+
165+ private readonly string _username ;
166+
167+ private readonly string _password ;
168+
169+ /// <summary>
170+ /// Initializes a new instance of the <see cref="WaitUntil" /> class.
171+ /// </summary>
172+ /// <param name="configuration">The container configuration.</param>
173+ public WaitUntil ( OpenSearchConfiguration configuration )
174+ {
175+ _tlsEnabled = configuration . TlsEnabled . GetValueOrDefault ( ) ;
176+ _username = configuration . Username ;
177+ _password = configuration . Password ;
178+ }
179+
180+ /// <inheritdoc />
181+ public async Task < bool > UntilAsync ( IContainer container )
182+ {
183+ using var httpMessageHandler = new HttpClientHandler ( ) ;
184+ httpMessageHandler . ServerCertificateCustomValidationCallback = ( _ , _ , _ , _ ) => true ;
185+
186+ var httpWaitStrategy = new HttpWaitStrategy ( )
187+ . UsingHttpMessageHandler ( httpMessageHandler )
188+ . UsingTls ( _tlsEnabled )
189+ . WithBasicAuthentication ( _username , _password )
190+ . ForPort ( OpenSearchRestApiPort ) ;
191+
192+ return await httpWaitStrategy . UntilAsync ( container )
193+ . ConfigureAwait ( false ) ;
194+ }
195+ }
196+ }
0 commit comments