1515import org .elasticsearch .cluster .ClusterStateListener ;
1616import org .elasticsearch .cluster .metadata .ProjectId ;
1717import org .elasticsearch .cluster .metadata .ProjectMetadata ;
18- import org .elasticsearch .cluster .metadata .RepositoryMetadata ;
1918import org .elasticsearch .common .settings .ProjectSecrets ;
2019import org .elasticsearch .common .settings .Settings ;
2120import org .elasticsearch .common .util .Maps ;
4140public class S3PerProjectClientManager implements ClusterStateListener {
4241
4342 private static final Logger logger = LogManager .getLogger (S3PerProjectClientManager .class );
43+ private static final String S3_SETTING_PREFIX = "s3." ;
4444
45- private final Settings settings ;
45+ private final Settings nodeS3Settings ;
4646 private final Function <S3ClientSettings , AmazonS3Reference > clientBuilder ;
4747 private final Executor executor ;
4848 // A map of projectId to clients holder. Adding to and removing from the map happen only in the cluster state listener thread.
@@ -51,7 +51,10 @@ public class S3PerProjectClientManager implements ClusterStateListener {
5151 private volatile SubscribableListener <Void > clientsCloseListener = null ;
5252
5353 S3PerProjectClientManager (Settings settings , Function <S3ClientSettings , AmazonS3Reference > clientBuilder , Executor executor ) {
54- this .settings = settings ;
54+ this .nodeS3Settings = Settings .builder ()
55+ .put (settings .getByPrefix (S3_SETTING_PREFIX ), false ) // not use any cluster scoped secrets
56+ .normalizePrefix (S3_SETTING_PREFIX )
57+ .build ();
5558 this .clientBuilder = clientBuilder ;
5659 this .executor = executor ;
5760 this .projectClientsHolders = new ConcurrentHashMap <>();
@@ -66,36 +69,48 @@ public void clusterChanged(ClusterChangedEvent event) {
6669 final Map <ProjectId , ProjectMetadata > currentProjects = event .state ().metadata ().projects ();
6770
6871 final var updatedPerProjectClients = new HashMap <ProjectId , ClientsHolder >();
72+ final List <ClientsHolder > clientsHoldersToClose = new ArrayList <>();
6973 for (var project : currentProjects .values ()) {
7074 final ProjectSecrets projectSecrets = project .custom (ProjectSecrets .TYPE );
71- if (projectSecrets == null ) {
72- // This can only happen when a node restarts, it will be processed again when file settings are loaded
75+ // Project secrets can be null when node restarts. It may not have s3 credentials if s3 is not used.
76+ if (projectSecrets == null || projectSecrets .getSettingNames ().stream ().noneMatch (key -> key .startsWith ("s3." ))) {
77+ // Most likely there won't be any existing client, but attempt to remove it anyway just in case
78+ final ClientsHolder removed = projectClientsHolders .remove (project .id ());
79+ if (removed != null ) {
80+ clientsHoldersToClose .add (removed );
81+ }
7382 continue ;
7483 }
84+
7585 final Settings currentSettings = Settings .builder ()
76- // merge with static settings such as max retries etc, exclude secure settings
86+ // merge with static settings such as max retries etc
7787 // TODO: We may need to update this if per-project settings decide to support hierarchical overrides
78- .put (settings , false ) // do not fallback to cluster scoped secrets
88+ .put (nodeS3Settings )
7989 .setSecureSettings (projectSecrets .getSettings ())
8090 .build ();
8191 final Map <String , S3ClientSettings > clientSettings = S3ClientSettings .load (currentSettings )
8292 .entrySet ()
8393 .stream ()
84- // Skip project clients that have no credentials configured. This should not happen in serverless since all clients should
85- // have credentials configured. But it is safer to skip them.
94+ // Skip project clients that have no credentials configured. This should not happen in serverless.
95+ // But it is safer to skip them and is also a more consistent behaviour with the cases when
96+ // project secrets are not present.
8697 .filter (entry -> entry .getValue ().credentials != null )
8798 .collect (Collectors .toUnmodifiableMap (Map .Entry ::getKey , Map .Entry ::getValue ));
8899
89- // TODO: clientSettings should not be empty, i.e. there should be at least one client configured
90- // Maybe log a warning if it is empty and continue. The project will not have usable client but that is probably ok.
100+ if (clientSettings .isEmpty ()) {
101+ // clientSettings should not be empty, i.e. there should be at least one client configured.
102+ // But if it does somehow happen, log a warning and continue. The project will not have usable client but that is ok.
103+ logger .warn ("Skipping project [{}] with no client settings" , project .id ());
104+ continue ;
105+ }
91106
92- // TODO: Building and comparing the whole S3ClientSettings may be inefficient, we could just compare the relevant secrets
107+ // TODO: If performance is an issue, we may consider comparing just the relevant project secrets for new or updated clients
108+ // and avoid building the clientSettings
93109 if (newOrUpdated (project .id (), clientSettings )) {
94- updatedPerProjectClients .put (project .id (), new ClientsHolder (clientSettings ));
110+ updatedPerProjectClients .put (project .id (), new ClientsHolder (project . id (), clientSettings ));
95111 }
96112 }
97113
98- final List <ClientsHolder > clientsHoldersToClose = new ArrayList <>();
99114 // Updated projects
100115 for (var projectId : updatedPerProjectClients .keySet ()) {
101116 final var old = projectClientsHolders .put (projectId , updatedPerProjectClients .get (projectId ));
@@ -133,12 +148,12 @@ private void closeClientsAsync(List<ClientsHolder> clientsHoldersToClose, Action
133148 });
134149 }
135150
136- public AmazonS3Reference client (ProjectId projectId , RepositoryMetadata repositoryMetadata ) {
151+ public AmazonS3Reference client (ProjectId projectId , String clientName ) {
152+ assert projectId != null && ProjectId .DEFAULT .equals (projectId ) == false : projectId ;
137153 final var clientsHolder = projectClientsHolders .get (projectId );
138154 if (clientsHolder == null ) {
139- throw new IllegalArgumentException ("project [" + projectId + "] does not exist " );
155+ throw new IllegalArgumentException ("no s3 client is configured for project [" + projectId + "]" );
140156 }
141- final String clientName = S3Repository .CLIENT_NAME .get (repositoryMetadata .settings ());
142157 return clientsHolder .client (clientName );
143158 }
144159
@@ -147,6 +162,7 @@ public AmazonS3Reference client(ProjectId projectId, RepositoryMetadata reposito
147162 * All clients for the project are closed and will be recreated on next access, also similar to S3Service#releaseCachedClients
148163 */
149164 public void releaseProjectClients (ProjectId projectId ) {
165+ assert projectId != null && ProjectId .DEFAULT .equals (projectId ) == false : projectId ;
150166 final var old = projectClientsHolders .get (projectId );
151167 if (old != null ) {
152168 old .clearCache ();
@@ -197,11 +213,13 @@ private boolean newOrUpdated(ProjectId projectId, Map<String, S3ClientSettings>
197213 */
198214 final class ClientsHolder implements Closeable {
199215 private final AtomicBoolean closed = new AtomicBoolean (false );
216+ private final ProjectId projectId ;
200217 private final Map <String , S3ClientSettings > clientSettings ;
201218 // Client name -> client reference
202219 private volatile Map <String , AmazonS3Reference > clientsCache = Collections .emptyMap ();
203220
204- ClientsHolder (Map <String , S3ClientSettings > clientSettings ) {
221+ ClientsHolder (ProjectId projectId , Map <String , S3ClientSettings > clientSettings ) {
222+ this .projectId = projectId ;
205223 this .clientSettings = clientSettings ;
206224 }
207225
@@ -219,7 +237,7 @@ AmazonS3Reference client(String clientName) {
219237 }
220238 final var settings = clientSettings .get (clientName );
221239 if (settings == null ) {
222- throw new IllegalArgumentException ("client [" + clientName + "] does not exist" );
240+ throw new IllegalArgumentException ("s3 client [" + clientName + "] does not exist for project [" + projectId + "] " );
223241 }
224242 synchronized (this ) {
225243 final var existing = clientsCache .get (clientName );
0 commit comments