Skip to content

Commit 4cc7958

Browse files
wilkinsonamhalbritterphilwebb
committed
Add ConnectionDetail support to Elasticsearch auto-configuration
Update Elasticsearch auto-configuration so that `ElasticsearchConnectionDetails` beans may be optionally used to provide connection details. See gh-34657 Co-Authored-By: Mortitz Halbritter <[email protected]> Co-Authored-By: Phillip Webb <[email protected]>
1 parent 9f187bb commit 4cc7958

File tree

3 files changed

+289
-49
lines changed

3 files changed

+289
-49
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.elasticsearch;
18+
19+
import java.net.URI;
20+
import java.net.URISyntaxException;
21+
import java.util.List;
22+
23+
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
24+
25+
/**
26+
* Details required to establish a connection to an Elasticsearch service.
27+
*
28+
* @author Moritz Halbritter
29+
* @author Andy Wilkinson
30+
* @author Phillip Webb
31+
* @since 3.1.0
32+
*/
33+
public interface ElasticsearchConnectionDetails extends ConnectionDetails {
34+
35+
/**
36+
* List of the Elasticsearch nodes to use.
37+
* @return list of the Elasticsearch nodes to use
38+
*/
39+
List<Node> getNodes();
40+
41+
/**
42+
* Username for authentication with Elasticsearch.
43+
* @return username for authentication with Elasticsearch or {@code null}
44+
*/
45+
default String getUsername() {
46+
return null;
47+
}
48+
49+
/**
50+
* Password for authentication with Elasticsearch.
51+
* @return password for authentication with Elasticsearch or {@code null}
52+
*/
53+
default String getPassword() {
54+
return null;
55+
}
56+
57+
/**
58+
* Prefix added to the path of every request sent to Elasticsearch.
59+
* @return prefix added to the path of every request sent to Elasticsearch or
60+
* {@code null}
61+
*/
62+
default String getPathPrefix() {
63+
return null;
64+
}
65+
66+
/**
67+
* An Elasticsearch node.
68+
*
69+
* @param hostname the hostname
70+
* @param port the port
71+
* @param protocol the protocol
72+
* @param username the username or {@code null}
73+
* @param password the password or {@code null}
74+
*/
75+
record Node(String hostname, int port, Node.Protocol protocol, String username, String password) {
76+
77+
public Node(String host, int port, Node.Protocol protocol) {
78+
this(host, port, protocol, null, null);
79+
}
80+
81+
URI toUri() {
82+
try {
83+
return new URI(this.protocol.getScheme(), userInfo(), this.hostname, this.port, null, null, null);
84+
}
85+
catch (URISyntaxException ex) {
86+
throw new IllegalStateException("Can't construct URI", ex);
87+
}
88+
}
89+
90+
private String userInfo() {
91+
if (this.username == null) {
92+
return null;
93+
}
94+
return (this.password != null) ? (this.username + ":" + this.password) : this.username;
95+
}
96+
97+
/**
98+
* Connection protocol.
99+
*/
100+
public enum Protocol {
101+
102+
/**
103+
* HTTP.
104+
*/
105+
HTTP("http"),
106+
107+
/**
108+
* HTTPS.
109+
*/
110+
HTTPS("https");
111+
112+
private final String scheme;
113+
114+
Protocol(String scheme) {
115+
this.scheme = scheme;
116+
}
117+
118+
String getScheme() {
119+
return this.scheme;
120+
}
121+
122+
static Protocol forScheme(String scheme) {
123+
for (Protocol protocol : values()) {
124+
if (protocol.scheme.equals(scheme)) {
125+
return protocol;
126+
}
127+
}
128+
throw new IllegalArgumentException("Unknown scheme '" + scheme + "'");
129+
}
130+
131+
}
132+
133+
}
134+
135+
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java

Lines changed: 91 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
package org.springframework.boot.autoconfigure.elasticsearch;
1818

1919
import java.net.URI;
20-
import java.net.URISyntaxException;
2120
import java.time.Duration;
21+
import java.util.List;
22+
import java.util.stream.Stream;
2223

2324
import org.apache.http.HttpHost;
2425
import org.apache.http.auth.AuthScope;
@@ -37,6 +38,8 @@
3738
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
3839
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
3940
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
41+
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node;
42+
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node.Protocol;
4043
import org.springframework.boot.context.properties.PropertyMapper;
4144
import org.springframework.context.annotation.Bean;
4245
import org.springframework.context.annotation.Configuration;
@@ -47,6 +50,9 @@
4750
*
4851
* @author Stephane Nicoll
4952
* @author Filip Hrisafov
53+
* @author Moritz Halbritter
54+
* @author Andy Wilkinson
55+
* @author Phillip Webb
5056
*/
5157
class ElasticsearchRestClientConfigurations {
5258

@@ -56,20 +62,27 @@ static class RestClientBuilderConfiguration {
5662

5763
private final ElasticsearchProperties properties;
5864

59-
RestClientBuilderConfiguration(ElasticsearchProperties properties) {
65+
private final ElasticsearchConnectionDetails connectionDetails;
66+
67+
RestClientBuilderConfiguration(ElasticsearchProperties properties,
68+
ObjectProvider<ElasticsearchConnectionDetails> connectionDetails) {
6069
this.properties = properties;
70+
this.connectionDetails = connectionDetails
71+
.getIfAvailable(() -> new PropertiesElasticsearchConnectionDetails(properties));
6172
}
6273

6374
@Bean
6475
RestClientBuilderCustomizer defaultRestClientBuilderCustomizer() {
65-
return new DefaultRestClientBuilderCustomizer(this.properties);
76+
return new DefaultRestClientBuilderCustomizer(this.properties, this.connectionDetails);
6677
}
6778

6879
@Bean
6980
RestClientBuilder elasticsearchRestClientBuilder(
7081
ObjectProvider<RestClientBuilderCustomizer> builderCustomizers) {
71-
HttpHost[] hosts = this.properties.getUris().stream().map(this::createHttpHost).toArray(HttpHost[]::new);
72-
RestClientBuilder builder = RestClient.builder(hosts);
82+
RestClientBuilder builder = RestClient.builder(this.connectionDetails.getNodes()
83+
.stream()
84+
.map((node) -> new HttpHost(node.hostname(), node.port(), node.protocol().getScheme()))
85+
.toArray(HttpHost[]::new));
7386
builder.setHttpClientConfigCallback((httpClientBuilder) -> {
7487
builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(httpClientBuilder));
7588
return httpClientBuilder;
@@ -78,36 +91,14 @@ RestClientBuilder elasticsearchRestClientBuilder(
7891
builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(requestConfigBuilder));
7992
return requestConfigBuilder;
8093
});
81-
if (this.properties.getPathPrefix() != null) {
82-
builder.setPathPrefix(this.properties.getPathPrefix());
94+
String pathPrefix = this.connectionDetails.getPathPrefix();
95+
if (pathPrefix != null) {
96+
builder.setPathPrefix(pathPrefix);
8397
}
8498
builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
8599
return builder;
86100
}
87101

88-
private HttpHost createHttpHost(String uri) {
89-
try {
90-
return createHttpHost(URI.create(uri));
91-
}
92-
catch (IllegalArgumentException ex) {
93-
return HttpHost.create(uri);
94-
}
95-
}
96-
97-
private HttpHost createHttpHost(URI uri) {
98-
if (!StringUtils.hasLength(uri.getUserInfo())) {
99-
return HttpHost.create(uri.toString());
100-
}
101-
try {
102-
return HttpHost.create(new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(),
103-
uri.getQuery(), uri.getFragment())
104-
.toString());
105-
}
106-
catch (URISyntaxException ex) {
107-
throw new IllegalStateException(ex);
108-
}
109-
}
110-
111102
}
112103

113104
@Configuration(proxyBeanMethods = false)
@@ -146,8 +137,12 @@ static class DefaultRestClientBuilderCustomizer implements RestClientBuilderCust
146137

147138
private final ElasticsearchProperties properties;
148139

149-
DefaultRestClientBuilderCustomizer(ElasticsearchProperties properties) {
140+
private final ElasticsearchConnectionDetails connectionDetails;
141+
142+
DefaultRestClientBuilderCustomizer(ElasticsearchProperties properties,
143+
ElasticsearchConnectionDetails connectionDetails) {
150144
this.properties = properties;
145+
this.connectionDetails = connectionDetails;
151146
}
152147

153148
@Override
@@ -156,7 +151,7 @@ public void customize(RestClientBuilder builder) {
156151

157152
@Override
158153
public void customize(HttpAsyncClientBuilder builder) {
159-
builder.setDefaultCredentialsProvider(new PropertiesCredentialsProvider(this.properties));
154+
builder.setDefaultCredentialsProvider(new ConnectionDetailsCredentialsProvider(this.connectionDetails));
160155
map.from(this.properties::isSocketKeepAlive)
161156
.to((keepAlive) -> builder
162157
.setDefaultIOReactorConfig(IOReactorConfig.custom().setSoKeepAlive(keepAlive).build()));
@@ -176,28 +171,20 @@ public void customize(RequestConfig.Builder builder) {
176171

177172
}
178173

179-
private static class PropertiesCredentialsProvider extends BasicCredentialsProvider {
174+
private static class ConnectionDetailsCredentialsProvider extends BasicCredentialsProvider {
180175

181-
PropertiesCredentialsProvider(ElasticsearchProperties properties) {
182-
if (StringUtils.hasText(properties.getUsername())) {
183-
Credentials credentials = new UsernamePasswordCredentials(properties.getUsername(),
184-
properties.getPassword());
176+
ConnectionDetailsCredentialsProvider(ElasticsearchConnectionDetails connectionDetails) {
177+
String username = connectionDetails.getUsername();
178+
if (StringUtils.hasText(username)) {
179+
Credentials credentials = new UsernamePasswordCredentials(username, connectionDetails.getPassword());
185180
setCredentials(AuthScope.ANY, credentials);
186181
}
187-
properties.getUris()
188-
.stream()
189-
.map(this::toUri)
190-
.filter(this::hasUserInfo)
191-
.forEach(this::addUserInfoCredentials);
182+
Stream<URI> uris = getUris(connectionDetails);
183+
uris.filter(this::hasUserInfo).forEach(this::addUserInfoCredentials);
192184
}
193185

194-
private URI toUri(String uri) {
195-
try {
196-
return URI.create(uri);
197-
}
198-
catch (IllegalArgumentException ex) {
199-
return null;
200-
}
186+
private Stream<URI> getUris(ElasticsearchConnectionDetails connectionDetails) {
187+
return connectionDetails.getNodes().stream().map(Node::toUri);
201188
}
202189

203190
private boolean hasUserInfo(URI uri) {
@@ -222,4 +209,59 @@ private Credentials createUserInfoCredentials(String userInfo) {
222209

223210
}
224211

212+
/**
213+
* Adapts {@link ElasticsearchProperties} to {@link ElasticsearchConnectionDetails}.
214+
*/
215+
private static class PropertiesElasticsearchConnectionDetails implements ElasticsearchConnectionDetails {
216+
217+
private final ElasticsearchProperties properties;
218+
219+
PropertiesElasticsearchConnectionDetails(ElasticsearchProperties properties) {
220+
this.properties = properties;
221+
}
222+
223+
@Override
224+
public List<Node> getNodes() {
225+
return this.properties.getUris().stream().map(this::createNode).toList();
226+
}
227+
228+
@Override
229+
public String getUsername() {
230+
return this.properties.getUsername();
231+
}
232+
233+
@Override
234+
public String getPassword() {
235+
return this.properties.getPassword();
236+
}
237+
238+
@Override
239+
public String getPathPrefix() {
240+
return this.properties.getPathPrefix();
241+
}
242+
243+
private Node createNode(String uri) {
244+
if (!(uri.startsWith("http://") || uri.startsWith("https://"))) {
245+
uri = "http://" + uri;
246+
}
247+
return createNode(URI.create(uri));
248+
}
249+
250+
private Node createNode(URI uri) {
251+
String userInfo = uri.getUserInfo();
252+
Protocol protocol = Protocol.forScheme(uri.getScheme());
253+
if (!StringUtils.hasLength(userInfo)) {
254+
return new Node(uri.getHost(), uri.getPort(), protocol, null, null);
255+
}
256+
int separatorIndex = userInfo.indexOf(':');
257+
if (separatorIndex == -1) {
258+
return new Node(uri.getHost(), uri.getPort(), protocol, userInfo, null);
259+
}
260+
String[] components = userInfo.split(":");
261+
return new Node(uri.getHost(), uri.getPort(), protocol, components[0],
262+
(components.length > 1) ? components[1] : "");
263+
}
264+
265+
}
266+
225267
}

0 commit comments

Comments
 (0)