Skip to content

Commit 3db5ae0

Browse files
Update s3 secure settings (#28517)
Cache of clients by name which can be cleared when secure settings get updated.
1 parent 54eb461 commit 3db5ae0

20 files changed

+821
-439
lines changed

plugins/repository-s3/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ bundlePlugin {
5555
}
5656

5757
additionalTest('testRepositoryCreds'){
58-
include '**/RepositorySettingsCredentialsTests.class'
58+
include '**/RepositoryCredentialsTests.class'
5959
systemProperty 'es.allow_insecure_settings', 'true'
6060
}
6161

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.repositories.s3;
21+
22+
import org.elasticsearch.common.util.concurrent.AbstractRefCounted;
23+
24+
import com.amazonaws.services.s3.AmazonS3;
25+
import com.amazonaws.services.s3.AmazonS3Client;
26+
27+
import org.elasticsearch.common.lease.Releasable;
28+
29+
/**
30+
* Handles the shutdown of the wrapped {@link AmazonS3Client} using reference
31+
* counting.
32+
*/
33+
public class AmazonS3Reference extends AbstractRefCounted implements Releasable {
34+
35+
private final AmazonS3 client;
36+
37+
AmazonS3Reference(AmazonS3 client) {
38+
super("AWS_S3_CLIENT");
39+
this.client = client;
40+
}
41+
42+
/**
43+
* Call when the client is not needed anymore.
44+
*/
45+
@Override
46+
public void close() {
47+
decRef();
48+
}
49+
50+
/**
51+
* Returns the underlying `AmazonS3` client. All method calls are permitted BUT
52+
* NOT shutdown. Shutdown is called when reference count reaches 0.
53+
*/
54+
public AmazonS3 client() {
55+
return client;
56+
}
57+
58+
@Override
59+
protected void closeInternal() {
60+
client.shutdown();
61+
}
62+
63+
}

plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/AwsS3Service.java

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,30 @@
1919

2020
package org.elasticsearch.repositories.s3;
2121

22-
import com.amazonaws.services.s3.AmazonS3;
23-
import org.elasticsearch.common.component.LifecycleComponent;
24-
import org.elasticsearch.common.settings.Settings;
22+
import java.util.Map;
2523

26-
interface AwsS3Service extends LifecycleComponent {
24+
interface AwsS3Service {
2725

2826
/**
29-
* Creates an {@code AmazonS3} client from the given repository metadata and node settings.
27+
* Creates then caches an {@code AmazonS3} client using the current client
28+
* settings.
3029
*/
31-
AmazonS3 client(Settings repositorySettings);
30+
AmazonS3Reference client(String clientName);
31+
32+
/**
33+
* Updates settings for building clients. Future client requests will use the
34+
* new settings. Implementations SHOULD drop the client cache to prevent reusing
35+
* clients with old settings from cache.
36+
*
37+
* @param clientsSettings
38+
* the new settings
39+
* @return the old settings
40+
*/
41+
Map<String, S3ClientSettings> updateClientsSettings(Map<String, S3ClientSettings> clientsSettings);
42+
43+
/**
44+
* Releases cached clients. Subsequent client requests will recreate client
45+
* instances. Does not touch the client settings.
46+
*/
47+
void releaseCachedClients();
3248
}

plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/InternalAwsS3Service.java

Lines changed: 70 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -28,66 +28,88 @@
2828
import com.amazonaws.internal.StaticCredentialsProvider;
2929
import com.amazonaws.services.s3.AmazonS3;
3030
import com.amazonaws.services.s3.AmazonS3Client;
31+
3132
import org.apache.logging.log4j.Logger;
32-
import org.elasticsearch.ElasticsearchException;
3333
import org.elasticsearch.common.Strings;
34-
import org.elasticsearch.common.component.AbstractLifecycleComponent;
35-
import org.elasticsearch.common.logging.DeprecationLogger;
36-
import org.elasticsearch.common.settings.SecureString;
37-
import org.elasticsearch.common.settings.Setting;
34+
import org.elasticsearch.common.collect.MapBuilder;
35+
import org.elasticsearch.common.component.AbstractComponent;
3836
import org.elasticsearch.common.settings.Settings;
39-
40-
import java.util.HashMap;
4137
import java.util.Map;
42-
import java.util.function.Function;
43-
44-
45-
class InternalAwsS3Service extends AbstractLifecycleComponent implements AwsS3Service {
38+
import static java.util.Collections.emptyMap;
4639

47-
// pkg private for tests
48-
static final Setting<String> CLIENT_NAME = new Setting<>("client", "default", Function.identity());
4940

50-
private final Map<String, S3ClientSettings> clientsSettings;
41+
class InternalAwsS3Service extends AbstractComponent implements AwsS3Service {
5142

52-
private final Map<String, AmazonS3Client> clientsCache = new HashMap<>();
43+
private volatile Map<String, AmazonS3Reference> clientsCache = emptyMap();
44+
private volatile Map<String, S3ClientSettings> clientsSettings = emptyMap();
5345

54-
InternalAwsS3Service(Settings settings, Map<String, S3ClientSettings> clientsSettings) {
46+
InternalAwsS3Service(Settings settings) {
5547
super(settings);
56-
this.clientsSettings = clientsSettings;
5748
}
5849

50+
/**
51+
* Reloads the settings for the AmazonS3 client. New clients will be build using
52+
* these. Old clients are usable until released. On release they will be
53+
* destroyed contrary to being returned to the cache.
54+
*/
5955
@Override
60-
public synchronized AmazonS3 client(Settings repositorySettings) {
61-
String clientName = CLIENT_NAME.get(repositorySettings);
62-
AmazonS3Client client = clientsCache.get(clientName);
63-
if (client != null) {
64-
return client;
65-
}
56+
public synchronized Map<String, S3ClientSettings> updateClientsSettings(Map<String, S3ClientSettings> clientsSettings) {
57+
// shutdown all unused clients
58+
// others will shutdown on their respective release
59+
releaseCachedClients();
60+
final Map<String, S3ClientSettings> prevSettings = this.clientsSettings;
61+
this.clientsSettings = MapBuilder.newMapBuilder(clientsSettings).immutableMap();
62+
assert this.clientsSettings.containsKey("default") : "always at least have 'default'";
63+
// clients are built lazily by {@link client(String)}
64+
return prevSettings;
65+
}
6666

67-
S3ClientSettings clientSettings = clientsSettings.get(clientName);
68-
if (clientSettings == null) {
69-
throw new IllegalArgumentException("Unknown s3 client name [" + clientName + "]. Existing client configs: " +
70-
Strings.collectionToDelimitedString(clientsSettings.keySet(), ","));
67+
/**
68+
* Attempts to retrieve a client by name from the cache. If the client does not
69+
* exist it will be created.
70+
*/
71+
@Override
72+
public AmazonS3Reference client(String clientName) {
73+
AmazonS3Reference clientReference = clientsCache.get(clientName);
74+
if ((clientReference != null) && clientReference.tryIncRef()) {
75+
return clientReference;
7176
}
77+
synchronized (this) {
78+
clientReference = clientsCache.get(clientName);
79+
if ((clientReference != null) && clientReference.tryIncRef()) {
80+
return clientReference;
81+
}
82+
final S3ClientSettings clientSettings = clientsSettings.get(clientName);
83+
if (clientSettings == null) {
84+
throw new IllegalArgumentException("Unknown s3 client name [" + clientName + "]. Existing client configs: "
85+
+ Strings.collectionToDelimitedString(clientsSettings.keySet(), ","));
86+
}
87+
logger.debug("creating S3 client with client_name [{}], endpoint [{}]", clientName, clientSettings.endpoint);
88+
clientReference = new AmazonS3Reference(buildClient(clientSettings));
89+
clientReference.incRef();
90+
clientsCache = MapBuilder.newMapBuilder(clientsCache).put(clientName, clientReference).immutableMap();
91+
return clientReference;
92+
}
93+
}
7294

73-
logger.debug("creating S3 client with client_name [{}], endpoint [{}]", clientName, clientSettings.endpoint);
74-
75-
AWSCredentialsProvider credentials = buildCredentials(logger, deprecationLogger, clientSettings, repositorySettings);
76-
ClientConfiguration configuration = buildConfiguration(clientSettings);
77-
78-
client = new AmazonS3Client(credentials, configuration);
79-
95+
private AmazonS3 buildClient(S3ClientSettings clientSettings) {
96+
final AWSCredentialsProvider credentials = buildCredentials(logger, clientSettings);
97+
final ClientConfiguration configuration = buildConfiguration(clientSettings);
98+
final AmazonS3 client = buildClient(credentials, configuration);
8099
if (Strings.hasText(clientSettings.endpoint)) {
81100
client.setEndpoint(clientSettings.endpoint);
82101
}
83-
84-
clientsCache.put(clientName, client);
85102
return client;
86103
}
87104

105+
// proxy for testing
106+
AmazonS3 buildClient(AWSCredentialsProvider credentials, ClientConfiguration configuration) {
107+
return new AmazonS3Client(credentials, configuration);
108+
}
109+
88110
// pkg private for tests
89111
static ClientConfiguration buildConfiguration(S3ClientSettings clientSettings) {
90-
ClientConfiguration clientConfiguration = new ClientConfiguration();
112+
final ClientConfiguration clientConfiguration = new ClientConfiguration();
91113
// the response metadata cache is only there for diagnostics purposes,
92114
// but can force objects from every response to the old generation.
93115
clientConfiguration.setResponseMetadataCacheSize(0);
@@ -109,27 +131,8 @@ static ClientConfiguration buildConfiguration(S3ClientSettings clientSettings) {
109131
}
110132

111133
// pkg private for tests
112-
static AWSCredentialsProvider buildCredentials(Logger logger, DeprecationLogger deprecationLogger,
113-
S3ClientSettings clientSettings, Settings repositorySettings) {
114-
115-
116-
BasicAWSCredentials credentials = clientSettings.credentials;
117-
if (S3Repository.ACCESS_KEY_SETTING.exists(repositorySettings)) {
118-
if (S3Repository.SECRET_KEY_SETTING.exists(repositorySettings) == false) {
119-
throw new IllegalArgumentException("Repository setting [" + S3Repository.ACCESS_KEY_SETTING.getKey() +
120-
" must be accompanied by setting [" + S3Repository.SECRET_KEY_SETTING.getKey() + "]");
121-
}
122-
try (SecureString key = S3Repository.ACCESS_KEY_SETTING.get(repositorySettings);
123-
SecureString secret = S3Repository.SECRET_KEY_SETTING.get(repositorySettings)) {
124-
credentials = new BasicAWSCredentials(key.toString(), secret.toString());
125-
}
126-
// backcompat for reading keys out of repository settings
127-
deprecationLogger.deprecated("Using s3 access/secret key from repository settings. Instead " +
128-
"store these in named clients and the elasticsearch keystore for secure settings.");
129-
} else if (S3Repository.SECRET_KEY_SETTING.exists(repositorySettings)) {
130-
throw new IllegalArgumentException("Repository setting [" + S3Repository.SECRET_KEY_SETTING.getKey() +
131-
" must be accompanied by setting [" + S3Repository.ACCESS_KEY_SETTING.getKey() + "]");
132-
}
134+
static AWSCredentialsProvider buildCredentials(Logger logger, S3ClientSettings clientSettings) {
135+
final BasicAWSCredentials credentials = clientSettings.credentials;
133136
if (credentials == null) {
134137
logger.debug("Using instance profile credentials");
135138
return new PrivilegedInstanceProfileCredentialsProvider();
@@ -140,20 +143,15 @@ static AWSCredentialsProvider buildCredentials(Logger logger, DeprecationLogger
140143
}
141144

142145
@Override
143-
protected void doStart() throws ElasticsearchException {
144-
}
145-
146-
@Override
147-
protected void doStop() throws ElasticsearchException {
148-
}
149-
150-
@Override
151-
protected void doClose() throws ElasticsearchException {
152-
for (AmazonS3Client client : clientsCache.values()) {
153-
client.shutdown();
146+
public synchronized void releaseCachedClients() {
147+
// the clients will shutdown when they will not be used anymore
148+
for (final AmazonS3Reference clientReference : clientsCache.values()) {
149+
clientReference.decRef();
154150
}
155-
156-
// Ensure that IdleConnectionReaper is shutdown
151+
// clear previously cached clients, they will be build lazily
152+
clientsCache = emptyMap();
153+
// shutdown IdleConnectionReaper background thread
154+
// it will be restarted on new client usage
157155
IdleConnectionReaper.shutdown();
158156
}
159157

@@ -174,4 +172,5 @@ public void refresh() {
174172
SocketAccess.doPrivilegedVoid(credentials::refresh);
175173
}
176174
}
175+
177176
}

0 commit comments

Comments
 (0)