From 8024deac90ee5cb40ee51b09fddf58a854037f88 Mon Sep 17 00:00:00 2001 From: Sean Zatz Date: Fri, 5 Dec 2025 14:45:05 -0500 Subject: [PATCH 01/14] add default snapshot repo cluster setting --- .../common/settings/ClusterSettings.java | 1 + .../snapshots/SnapshotsService.java | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index 13b75fab557cc..64aafeea09b43 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -589,6 +589,7 @@ public void apply(Settings value, Settings current, Settings previous) { HandshakingTransportAddressConnector.PROBE_CONNECT_TIMEOUT_SETTING, HandshakingTransportAddressConnector.PROBE_HANDSHAKE_TIMEOUT_SETTING, SnapshotsService.MAX_CONCURRENT_SNAPSHOT_OPERATIONS_SETTING, + SnapshotsService.DEFAULT_SNAPSHOT_REPOSITORY_SETTING, RestoreService.REFRESH_REPO_UUID_ON_RESTORE_SETTING, FsHealthService.ENABLED_SETTING, FsHealthService.REFRESH_INTERVAL_SETTING, diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java index a25908d0acc6d..87c330083b27f 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java @@ -199,8 +199,22 @@ public final class SnapshotsService extends AbstractLifecycleComponent implement Setting.Property.Dynamic ); + /** + * Setting that specifies the default repository to use for snapshot operations when no repository is explicitly specified. + * If not set, snapshot operations will require an explicit repository name. + */ + public static final Setting DEFAULT_SNAPSHOT_REPOSITORY_SETTING = Setting.simpleString( + "snapshot.default_repository", + "", + Setting.Property.NodeScope, + Setting.Property.Dynamic, + Setting.Property.ServerlessPublic + ); + private volatile int maxConcurrentOperations; + private volatile String defaultRepository; + public SnapshotsService( Settings settings, ClusterService clusterService, @@ -225,6 +239,8 @@ public SnapshotsService( maxConcurrentOperations = MAX_CONCURRENT_SNAPSHOT_OPERATIONS_SETTING.get(settings); clusterService.getClusterSettings() .addSettingsUpdateConsumer(MAX_CONCURRENT_SNAPSHOT_OPERATIONS_SETTING, i -> maxConcurrentOperations = i); + defaultRepository = DEFAULT_SNAPSHOT_REPOSITORY_SETTING.get(settings); + clusterService.getClusterSettings().addSettingsUpdateConsumer(DEFAULT_SNAPSHOT_REPOSITORY_SETTING, s -> defaultRepository = s); } this.systemIndices = systemIndices; this.serializeProjectMetadata = serializeProjectMetadata; @@ -239,6 +255,15 @@ public SnapshotsService( this.shardSnapshotUpdateCompletionHandler = this::handleShardSnapshotUpdateCompletion; } + /** + * Gets the configured default snapshot repository. + * + * @return the default repository name, or an empty string if not configured + */ + public String getDefaultRepository() { + return defaultRepository; + } + /** * Same as {@link #createSnapshot(ProjectId, CreateSnapshotRequest, ActionListener)} but invokes its callback on completion of * the snapshot. From 8d6dfaa3437c8cb46f7aca9f5f27aa3e2e3defde Mon Sep 17 00:00:00 2001 From: Sean Zatz Date: Fri, 5 Dec 2025 18:38:45 -0500 Subject: [PATCH 02/14] Move to RepositoriesService. Add yaml rest tests. --- .../repositories/10_default_repository.yml | 186 ++++++++++++++++++ .../common/settings/ClusterSettings.java | 3 +- .../repositories/RepositoriesService.java | 85 ++++++++ .../snapshots/SnapshotsService.java | 25 --- 4 files changed, 273 insertions(+), 26 deletions(-) create mode 100644 rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/repositories/10_default_repository.yml diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/repositories/10_default_repository.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/repositories/10_default_repository.yml new file mode 100644 index 0000000000000..8b28754b27fb4 --- /dev/null +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/repositories/10_default_repository.yml @@ -0,0 +1,186 @@ +--- +setup: + # Create two test repositories without verification + - do: + snapshot.create_repository: + repository: test_repo_1 + verify: false + body: + type: fs + settings: + location: "test_repo_1_loc" + + - do: + snapshot.create_repository: + repository: test_repo_2 + verify: false + body: + type: fs + settings: + location: "test_repo_2_loc" + +--- +teardown: + # Clean up cluster settings + - do: + cluster.put_settings: + body: + persistent: + repositories.default_repository: null + + # Clean up repositories (if they still exist) + - do: + catch: missing + snapshot.delete_repository: + repository: test_repo_1 + + - do: + catch: missing + snapshot.delete_repository: + repository: test_repo_2 + +--- +"Set and validate default repository": + # Set first repo as default + - do: + cluster.put_settings: + body: + persistent: + repositories.default_repository: "test_repo_1" + + - match: { persistent.repositories.default_repository: "test_repo_1" } + + # Verify the setting is persisted + - do: + cluster.get_settings: + flat_settings: false + + - match: { persistent.repositories.default_repository: "test_repo_1" } + + # Clean up: clear the default setting + - do: + cluster.put_settings: + body: + persistent: + repositories.default_repository: null + +--- +"Cannot delete default repository": + # Set first repo as default + - do: + cluster.put_settings: + body: + persistent: + repositories.default_repository: "test_repo_1" + + # Try to delete the default repository - should fail + - do: + catch: /cannot delete the default repository/ + snapshot.delete_repository: + repository: test_repo_1 + + # Verify repo still exists + - do: + snapshot.get_repository: + repository: test_repo_1 + + - is_true: test_repo_1 + + # Clean up: clear the default setting + - do: + cluster.put_settings: + body: + persistent: + repositories.default_repository: null + +--- +"Cannot set non-existent repository as default": + # Try to set a non-existent repository as default + - do: + catch: /Repository \[non_existent_repo\] is not registered/ + cluster.put_settings: + body: + persistent: + repositories.default_repository: "non_existent_repo" + +--- +"Change default repository and delete previous default": + # Set first repo as default + - do: + cluster.put_settings: + body: + persistent: + repositories.default_repository: "test_repo_1" + + # Change default to second repo + - do: + cluster.put_settings: + body: + persistent: + repositories.default_repository: "test_repo_2" + + - match: { persistent.repositories.default_repository: "test_repo_2" } + + # Now we can delete first repo (no longer default) + - do: + snapshot.delete_repository: + repository: test_repo_1 + + - match: { acknowledged: true } + + # Verify first repo is gone + - do: + catch: missing + snapshot.get_repository: + repository: test_repo_1 + + # Verify second repo still exists and is default + - do: + snapshot.get_repository: + repository: test_repo_2 + + - is_true: test_repo_2 + + - do: + cluster.get_settings: {} + + - match: { persistent.repositories.default_repository: "test_repo_2" } + + # Clean up: clear the default setting + - do: + cluster.put_settings: + body: + persistent: + repositories.default_repository: null + +--- +"Clear default repository setting": + # Set a repository as default + - do: + cluster.put_settings: + body: + persistent: + repositories.default_repository: "test_repo_1" + + # Clear the default by setting to null + - do: + cluster.put_settings: + body: + persistent: + repositories.default_repository: null + + - match: { persistent: {} } + + # Verify the setting is cleared + - do: + cluster.get_settings: {} + + - is_false: persistent.repositories.default_repository + + # Now we can delete the repository + - do: + snapshot.delete_repository: + repository: test_repo_1 + + - match: { acknowledged: true } + diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index 64aafeea09b43..c1ce7f39b87e5 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -123,6 +123,7 @@ import org.elasticsearch.persistent.decider.EnableAssignmentDecider; import org.elasticsearch.plugins.PluginsService; import org.elasticsearch.readiness.ReadinessService; +import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.repositories.blobstore.BlobStoreRepository; import org.elasticsearch.repositories.fs.FsRepository; import org.elasticsearch.rest.BaseRestHandler; @@ -589,7 +590,7 @@ public void apply(Settings value, Settings current, Settings previous) { HandshakingTransportAddressConnector.PROBE_CONNECT_TIMEOUT_SETTING, HandshakingTransportAddressConnector.PROBE_HANDSHAKE_TIMEOUT_SETTING, SnapshotsService.MAX_CONCURRENT_SNAPSHOT_OPERATIONS_SETTING, - SnapshotsService.DEFAULT_SNAPSHOT_REPOSITORY_SETTING, + RepositoriesService.DEFAULT_REPOSITORY_SETTING, RestoreService.REFRESH_REPO_UUID_ON_RESTORE_SETTING, FsHealthService.ENABLED_SETTING, FsHealthService.REFRESH_INTERVAL_SETTING, diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java index cd1d072fcf1ea..3dcf1a2d51050 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java @@ -111,6 +111,16 @@ public class RepositoriesService extends AbstractLifecycleComponent implements C Setting.Property.NodeScope ); + /** + * Setting that specifies the default repository to use for snapshot and restore operations when no repository is explicitly + * specified. If not set, snapshot and restore operations will require an explicit repository name. + */ + public static final Setting DEFAULT_REPOSITORY_SETTING = Setting.simpleString( + "repositories.default_repository", + Setting.Property.NodeScope, + Setting.Property.Dynamic + ); + private final Map typesRegistry; private final Map internalTypesRegistry; @@ -125,6 +135,8 @@ public class RepositoriesService extends AbstractLifecycleComponent implements C private final List> preRestoreChecks; + private volatile String defaultRepository; + @SuppressWarnings("this-escape") public RepositoriesService( Settings settings, @@ -154,9 +166,63 @@ public RepositoriesService( threadPool.relativeTimeInMillisSupplier() ); this.preRestoreChecks = preRestoreChecks; + this.defaultRepository = DEFAULT_REPOSITORY_SETTING.get(settings); + if (DiscoveryNode.isMasterNode(settings)) { + clusterService.getClusterSettings() + .addSettingsUpdateConsumer(DEFAULT_REPOSITORY_SETTING, this::setDefaultRepository, this::validateDefaultRepository); + } snapshotMetrics.createSnapshotShardsInProgressMetric(this::getShardSnapshotsInProgress); } + /** + * Gets the configured default repository. + * + * @return the default repository name, or an empty string if not configured + */ + public String getDefaultRepository() { + return defaultRepository; + } + + /** + * Sets the default repository value. + * This is called by the settings update consumer when the setting changes. + * + * @param repositoryName the new default repository name + */ + private void setDefaultRepository(String repositoryName) { + this.defaultRepository = repositoryName; + } + + /** + * Validates that the default repository exists and is registered. + * This validator is called when the default repository setting is updated. + * Empty strings are allowed to clear the default repository setting. + * + * @param repositoryName the repository name to validate + * @throws IllegalArgumentException if the repository name is not empty and the repository doesn't exist + */ + private void validateDefaultRepository(String repositoryName) { + // Allow empty string to clear the setting + if (Strings.isEmpty(repositoryName)) { + logger.info("Default repository cleared"); + return; + } + + Repository repository = repositoryOrNull(ProjectId.DEFAULT, repositoryName); + if (repository == null) { + throw new IllegalArgumentException( + "Repository [" + + repositoryName + + "] is not registered. " + + "Cannot set as default repository. Please register the repository first using PUT /_snapshot/" + + repositoryName + ); + } + + // Repository exists, validation passed + logger.info("Default repository set to [{}]", repositoryName); + } + /** * Registers new repository in the cluster *

@@ -568,6 +634,7 @@ public ClusterState execute(ClusterState currentState) { for (RepositoryMetadata repositoryMetadata : repositories.repositories()) { if (Regex.simpleMatch(request.name(), repositoryMetadata.name())) { ensureRepositoryNotInUse(projectState, repositoryMetadata.name()); + ensureRepositoryNotDefault(currentState, repositoryMetadata.name()); ensureNoSearchableSnapshotsIndicesInUse(currentState, repositoryMetadata); deletedRepositories.add(repositoryMetadata.name()); changed = true; @@ -1135,6 +1202,24 @@ private static void ensureRepositoryNotInUse(ProjectState projectState, String r } } + /** + * Ensures that the repository being deleted is not currently set as the default repository. + * + * @param currentState the current cluster state + * @param repository the repository name to check + * @throws RepositoryException if the repository is the default repository + */ + private static void ensureRepositoryNotDefault(ClusterState currentState, String repository) { + String defaultRepository = DEFAULT_REPOSITORY_SETTING.get(currentState.metadata().settings()); + if (Strings.isEmpty(defaultRepository) == false && repository.equals(defaultRepository)) { + throw newRepositoryConflictException( + repository, + "cannot delete the default repository. Please change the default repository cluster setting " + + "repositories.default_repository before deleting this repository." + ); + } + } + public static boolean isReadOnly(Settings repositorySettings) { return Boolean.TRUE.equals(repositorySettings.getAsBoolean(BlobStoreRepository.READONLY_SETTING_KEY, null)); } diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java index 87c330083b27f..a25908d0acc6d 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java @@ -199,22 +199,8 @@ public final class SnapshotsService extends AbstractLifecycleComponent implement Setting.Property.Dynamic ); - /** - * Setting that specifies the default repository to use for snapshot operations when no repository is explicitly specified. - * If not set, snapshot operations will require an explicit repository name. - */ - public static final Setting DEFAULT_SNAPSHOT_REPOSITORY_SETTING = Setting.simpleString( - "snapshot.default_repository", - "", - Setting.Property.NodeScope, - Setting.Property.Dynamic, - Setting.Property.ServerlessPublic - ); - private volatile int maxConcurrentOperations; - private volatile String defaultRepository; - public SnapshotsService( Settings settings, ClusterService clusterService, @@ -239,8 +225,6 @@ public SnapshotsService( maxConcurrentOperations = MAX_CONCURRENT_SNAPSHOT_OPERATIONS_SETTING.get(settings); clusterService.getClusterSettings() .addSettingsUpdateConsumer(MAX_CONCURRENT_SNAPSHOT_OPERATIONS_SETTING, i -> maxConcurrentOperations = i); - defaultRepository = DEFAULT_SNAPSHOT_REPOSITORY_SETTING.get(settings); - clusterService.getClusterSettings().addSettingsUpdateConsumer(DEFAULT_SNAPSHOT_REPOSITORY_SETTING, s -> defaultRepository = s); } this.systemIndices = systemIndices; this.serializeProjectMetadata = serializeProjectMetadata; @@ -255,15 +239,6 @@ public SnapshotsService( this.shardSnapshotUpdateCompletionHandler = this::handleShardSnapshotUpdateCompletion; } - /** - * Gets the configured default snapshot repository. - * - * @return the default repository name, or an empty string if not configured - */ - public String getDefaultRepository() { - return defaultRepository; - } - /** * Same as {@link #createSnapshot(ProjectId, CreateSnapshotRequest, ActionListener)} but invokes its callback on completion of * the snapshot. From 35467c53dcdda0d15e7d0950f6a3b2f9cb8a9b2b Mon Sep 17 00:00:00 2001 From: Sean Zatz Date: Fri, 5 Dec 2025 18:48:51 -0500 Subject: [PATCH 03/14] Update server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../org/elasticsearch/repositories/RepositoriesService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java index 3dcf1a2d51050..4dad957a9f4a3 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java @@ -117,7 +117,7 @@ public class RepositoriesService extends AbstractLifecycleComponent implements C */ public static final Setting DEFAULT_REPOSITORY_SETTING = Setting.simpleString( "repositories.default_repository", - Setting.Property.NodeScope, + Setting.Property.ClusterScope, Setting.Property.Dynamic ); From 004eadb03009ffe905de92708ce70f24ead2b9e7 Mon Sep 17 00:00:00 2001 From: Sean Zatz Date: Fri, 5 Dec 2025 18:49:44 -0500 Subject: [PATCH 04/14] Update server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../org/elasticsearch/repositories/RepositoriesService.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java index 4dad957a9f4a3..12bb866a14557 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java @@ -167,10 +167,8 @@ public RepositoriesService( ); this.preRestoreChecks = preRestoreChecks; this.defaultRepository = DEFAULT_REPOSITORY_SETTING.get(settings); - if (DiscoveryNode.isMasterNode(settings)) { - clusterService.getClusterSettings() - .addSettingsUpdateConsumer(DEFAULT_REPOSITORY_SETTING, this::setDefaultRepository, this::validateDefaultRepository); - } + clusterService.getClusterSettings() + .addSettingsUpdateConsumer(DEFAULT_REPOSITORY_SETTING, this::setDefaultRepository, this::validateDefaultRepository); snapshotMetrics.createSnapshotShardsInProgressMetric(this::getShardSnapshotsInProgress); } From 67011c180ff0d5257c69c779577f977f0f5462f2 Mon Sep 17 00:00:00 2001 From: Sean Zatz Date: Fri, 5 Dec 2025 19:17:36 -0500 Subject: [PATCH 05/14] revert copilot change --- .../org/elasticsearch/repositories/RepositoriesService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java index 12bb866a14557..66bc06f8c15dd 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java @@ -117,7 +117,7 @@ public class RepositoriesService extends AbstractLifecycleComponent implements C */ public static final Setting DEFAULT_REPOSITORY_SETTING = Setting.simpleString( "repositories.default_repository", - Setting.Property.ClusterScope, + Setting.Property.NodeScope, Setting.Property.Dynamic ); From d4fa7a0fa53f5b330c6b2233e5288353665a05c2 Mon Sep 17 00:00:00 2001 From: Sean Zatz Date: Fri, 5 Dec 2025 21:50:09 -0500 Subject: [PATCH 06/14] add node feature for default repo --- .../repositories/10_default_repository.yml | 4 +++ server/src/main/java/module-info.java | 1 + .../repositories/RepositoriesFeatures.java | 32 +++++++++++++++++++ ...lasticsearch.features.FeatureSpecification | 1 + 4 files changed, 38 insertions(+) create mode 100644 server/src/main/java/org/elasticsearch/repositories/RepositoriesFeatures.java diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/repositories/10_default_repository.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/repositories/10_default_repository.yml index 8b28754b27fb4..a9ff07c090990 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/repositories/10_default_repository.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/repositories/10_default_repository.yml @@ -1,5 +1,9 @@ --- setup: + - requires: + cluster_features: ["repositories.default_repository_setting"] + reason: "Default repository setting was introduced in 9.2.0" + # Create two test repositories without verification - do: snapshot.create_repository: diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java index 06808013f36a2..d6d002377eebf 100644 --- a/server/src/main/java/module-info.java +++ b/server/src/main/java/module-info.java @@ -427,6 +427,7 @@ with org.elasticsearch.action.bulk.BulkFeatures, org.elasticsearch.features.InfrastructureFeatures, + org.elasticsearch.repositories.RepositoriesFeatures, org.elasticsearch.rest.action.admin.cluster.ClusterRerouteFeatures, org.elasticsearch.rest.action.admin.cluster.GetSnapshotsFeatures, org.elasticsearch.index.mapper.MapperFeatures, diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesFeatures.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesFeatures.java new file mode 100644 index 0000000000000..ca72c84ac5e9f --- /dev/null +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesFeatures.java @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.repositories; + +import org.elasticsearch.features.FeatureSpecification; +import org.elasticsearch.features.NodeFeature; + +import java.util.Set; + +/** + * Provides features for repositories functionality. + */ +public class RepositoriesFeatures implements FeatureSpecification { + + /** + * Feature for the default repository cluster setting. + * Introduced in 9.2.0 to allow setting a default repository for snapshot and restore operations. + */ + public static final NodeFeature DEFAULT_REPOSITORY_SETTING = new NodeFeature("repositories.default_repository_setting"); + + @Override + public Set getFeatures() { + return Set.of(DEFAULT_REPOSITORY_SETTING); + } +} diff --git a/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification b/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification index 42bf3c942daaf..35a4d0ce229d4 100644 --- a/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification +++ b/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification @@ -9,6 +9,7 @@ org.elasticsearch.action.bulk.BulkFeatures org.elasticsearch.features.InfrastructureFeatures +org.elasticsearch.repositories.RepositoriesFeatures org.elasticsearch.rest.action.admin.cluster.ClusterRerouteFeatures org.elasticsearch.rest.action.admin.cluster.GetSnapshotsFeatures org.elasticsearch.index.IndexFeatures From e11794078808cfbee434c0d3db8d71129771538d Mon Sep 17 00:00:00 2001 From: Sean Zatz Date: Sat, 6 Dec 2025 01:12:20 -0500 Subject: [PATCH 07/14] add support for multi-project --- .../repositories/RepositoriesService.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java index 66bc06f8c15dd..01db89645ee1b 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java @@ -208,16 +208,24 @@ private void validateDefaultRepository(String repositoryName) { Repository repository = repositoryOrNull(ProjectId.DEFAULT, repositoryName); if (repository == null) { - throw new IllegalArgumentException( - "Repository [" - + repositoryName - + "] is not registered. " - + "Cannot set as default repository. Please register the repository first using PUT /_snapshot/" - + repositoryName - ); + // If not in DEFAULT, check all other projects + boolean found = repositories.values().stream().anyMatch(projectRepos -> projectRepos.containsKey(repositoryName)); + if (found == false) { + // Check internal repositories + found = internalRepositories.values().stream().anyMatch(projectRepos -> projectRepos.containsKey(repositoryName)); + } + + if (found == false) { + throw new IllegalArgumentException( + "Repository [" + + repositoryName + + "] is not registered. " + + "Cannot set as default repository. Please register the repository first using PUT /_snapshot/" + + repositoryName + ); + } } - // Repository exists, validation passed logger.info("Default repository set to [{}]", repositoryName); } From d8acbba1196ddcdcca8d3cafba260389b05abb12 Mon Sep 17 00:00:00 2001 From: Sean Zatz Date: Sat, 6 Dec 2025 13:54:29 -0500 Subject: [PATCH 08/14] remove mocking from test suite for cluster settings --- .../elasticsearch/repositories/RepositoriesModuleTests.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/repositories/RepositoriesModuleTests.java b/server/src/test/java/org/elasticsearch/repositories/RepositoriesModuleTests.java index 9fb575f5a6b79..73dbe282d7129 100644 --- a/server/src/test/java/org/elasticsearch/repositories/RepositoriesModuleTests.java +++ b/server/src/test/java/org/elasticsearch/repositories/RepositoriesModuleTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.MockBigArrays; import org.elasticsearch.env.Environment; @@ -54,6 +55,9 @@ public void setUp() throws Exception { transportService = mock(TransportService.class); when(transportService.getThreadPool()).thenReturn(threadPool); clusterService = mock(ClusterService.class); + // ClusterSettings is a final class, so we need to create a real instance instead of mocking + ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); + when(clusterService.getClusterSettings()).thenReturn(clusterSettings); recoverySettings = mock(RecoverySettings.class); plugin1 = mock(RepositoryPlugin.class); plugin2 = mock(RepositoryPlugin.class); @@ -94,7 +98,7 @@ public void testCanRegisterTwoRepositoriesWithDifferentTypes() { repoPlugins, nodeClient, threadPool, - mock(ClusterService.class), + clusterService, MockBigArrays.NON_RECYCLING_INSTANCE, contentRegistry, recoverySettings, From c12a1fae0607f76196599dacab2dc17a8b7002f9 Mon Sep 17 00:00:00 2001 From: Sean Zatz Date: Sat, 6 Dec 2025 20:40:27 -0500 Subject: [PATCH 09/14] fix npe in failing tests --- .../reservedstate/ReservedRepositoryActionTests.java | 8 +++++++- .../IndicesClusterStateServiceRandomUpdatesTests.java | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/repositories/reservedstate/ReservedRepositoryActionTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/repositories/reservedstate/ReservedRepositoryActionTests.java index b1728aeddadaf..19266a4a2d5f2 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/repositories/reservedstate/ReservedRepositoryActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/repositories/reservedstate/ReservedRepositoryActionTests.java @@ -17,6 +17,7 @@ import org.elasticsearch.cluster.metadata.ProjectMetadata; import org.elasticsearch.cluster.metadata.RepositoryMetadata; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.repositories.Repository; @@ -39,6 +40,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; /** * Tests that the ReservedRepositoryAction does validation, can add and remove repositories @@ -144,10 +146,14 @@ public Repository create(ProjectId projectId, RepositoryMetadata metadata) { }; ThreadPool threadPool = mock(ThreadPool.class); + ClusterService clusterService = mock(ClusterService.class); + when(clusterService.getClusterSettings()).thenReturn( + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS) + ); RepositoriesService repositoriesService = spy( new RepositoriesService( Settings.EMPTY, - mock(ClusterService.class), + clusterService, Map.of(), Map.of("fs", fsFactory), threadPool, diff --git a/server/src/test/java/org/elasticsearch/indices/cluster/IndicesClusterStateServiceRandomUpdatesTests.java b/server/src/test/java/org/elasticsearch/indices/cluster/IndicesClusterStateServiceRandomUpdatesTests.java index bc60cb9699a53..ddd3d2b19f9af 100644 --- a/server/src/test/java/org/elasticsearch/indices/cluster/IndicesClusterStateServiceRandomUpdatesTests.java +++ b/server/src/test/java/org/elasticsearch/indices/cluster/IndicesClusterStateServiceRandomUpdatesTests.java @@ -38,6 +38,7 @@ import org.elasticsearch.cluster.routing.allocation.FailedShard; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.index.Index; @@ -542,6 +543,9 @@ private IndicesClusterStateService createIndicesClusterStateService( Collections.emptySet() ); final ClusterService clusterService = mock(ClusterService.class); + when(clusterService.getClusterSettings()).thenReturn( + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS) + ); final NodeClient client = mock(NodeClient.class); final RepositoriesService repositoriesService = new RepositoriesService( settings, From fd0c58d50fdaf72d5c5f0234925313e615c1366d Mon Sep 17 00:00:00 2001 From: Sean Zatz Date: Mon, 8 Dec 2025 11:13:23 -0500 Subject: [PATCH 10/14] Update comments --- .../org/elasticsearch/repositories/RepositoriesService.java | 4 ++-- .../elasticsearch/repositories/RepositoriesModuleTests.java | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java index 01db89645ee1b..0e4d9f6288dab 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java @@ -113,7 +113,7 @@ public class RepositoriesService extends AbstractLifecycleComponent implements C /** * Setting that specifies the default repository to use for snapshot and restore operations when no repository is explicitly - * specified. If not set, snapshot and restore operations will require an explicit repository name. + * specified. If not set, some snapshot and restore operations may require an explicit repository name. */ public static final Setting DEFAULT_REPOSITORY_SETTING = Setting.simpleString( "repositories.default_repository", @@ -192,7 +192,7 @@ private void setDefaultRepository(String repositoryName) { } /** - * Validates that the default repository exists and is registered. + * Validates that the default repository is registered. * This validator is called when the default repository setting is updated. * Empty strings are allowed to clear the default repository setting. * diff --git a/server/src/test/java/org/elasticsearch/repositories/RepositoriesModuleTests.java b/server/src/test/java/org/elasticsearch/repositories/RepositoriesModuleTests.java index 73dbe282d7129..f11d271aa06ff 100644 --- a/server/src/test/java/org/elasticsearch/repositories/RepositoriesModuleTests.java +++ b/server/src/test/java/org/elasticsearch/repositories/RepositoriesModuleTests.java @@ -55,7 +55,6 @@ public void setUp() throws Exception { transportService = mock(TransportService.class); when(transportService.getThreadPool()).thenReturn(threadPool); clusterService = mock(ClusterService.class); - // ClusterSettings is a final class, so we need to create a real instance instead of mocking ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); when(clusterService.getClusterSettings()).thenReturn(clusterSettings); recoverySettings = mock(RecoverySettings.class); From 967739dabb02afef457521c1be386fb9d83b7969 Mon Sep 17 00:00:00 2001 From: Sean Zatz Date: Mon, 8 Dec 2025 11:26:42 -0500 Subject: [PATCH 11/14] Refactor validate function. --- .../repositories/RepositoriesService.java | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java index 0e4d9f6288dab..cd66b1113f358 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java @@ -200,35 +200,40 @@ private void setDefaultRepository(String repositoryName) { * @throws IllegalArgumentException if the repository name is not empty and the repository doesn't exist */ private void validateDefaultRepository(String repositoryName) { - // Allow empty string to clear the setting if (Strings.isEmpty(repositoryName)) { logger.info("Default repository cleared"); return; } - - Repository repository = repositoryOrNull(ProjectId.DEFAULT, repositoryName); - if (repository == null) { - // If not in DEFAULT, check all other projects - boolean found = repositories.values().stream().anyMatch(projectRepos -> projectRepos.containsKey(repositoryName)); - if (found == false) { - // Check internal repositories - found = internalRepositories.values().stream().anyMatch(projectRepos -> projectRepos.containsKey(repositoryName)); - } - - if (found == false) { - throw new IllegalArgumentException( - "Repository [" - + repositoryName - + "] is not registered. " - + "Cannot set as default repository. Please register the repository first using PUT /_snapshot/" - + repositoryName - ); - } + if (isRepositoryRegistered(repositoryName) == false) { + throw new IllegalArgumentException( + "Repository [" + + repositoryName + + "] is not registered. " + + "Cannot set as default repository. Please register the repository prior to setting as default using PUT /_snapshot/" + + repositoryName + ); } - logger.info("Default repository set to [{}]", repositoryName); } + /** + * Checks if a repository is registered in the default project, any other project, or internal repositories. + * + * @param repositoryName the repository name to check + * @return true if the repository is registered, false otherwise + */ + private boolean isRepositoryRegistered(String repositoryName) { + if (repositoryOrNull(ProjectId.DEFAULT, repositoryName) != null) { + return true; + } + boolean found = repositories.values().stream().anyMatch(projectRepos -> projectRepos.containsKey(repositoryName)); + if (found) { + return true; + } + found = internalRepositories.values().stream().anyMatch(projectRepos -> projectRepos.containsKey(repositoryName)); + return found; + } + /** * Registers new repository in the cluster *

From 29d8ac6723316827ac004d2b1801dad906c3a504 Mon Sep 17 00:00:00 2001 From: Sean Zatz Date: Tue, 9 Dec 2025 18:42:51 -0500 Subject: [PATCH 12/14] address PR comments --- .../repositories/10_default_repository.yml | 32 ++++--------------- server/src/main/java/module-info.java | 1 - .../repositories/RepositoriesFeatures.java | 32 ------------------- .../repositories/RepositoriesService.java | 2 +- .../RestClusterUpdateSettingsAction.java | 7 ++++ ...lasticsearch.features.FeatureSpecification | 1 - 6 files changed, 15 insertions(+), 60 deletions(-) delete mode 100644 server/src/main/java/org/elasticsearch/repositories/RepositoriesFeatures.java diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/repositories/10_default_repository.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/repositories/10_default_repository.yml index a9ff07c090990..4f7d4d8ce380b 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/repositories/10_default_repository.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/repositories/10_default_repository.yml @@ -1,8 +1,12 @@ --- setup: - requires: - cluster_features: ["repositories.default_repository_setting"] - reason: "Default repository setting was introduced in 9.2.0" + test_runner_features: [capabilities] + capabilities: + - method: PUT + path: /_cluster/settings + capabilities: ["repositories.default_repository_setting"] + reason: "Default repository setting capability check" # Create two test repositories without verification - do: @@ -56,18 +60,10 @@ teardown: # Verify the setting is persisted - do: - cluster.get_settings: - flat_settings: false + cluster.get_settings: {} - match: { persistent.repositories.default_repository: "test_repo_1" } - # Clean up: clear the default setting - - do: - cluster.put_settings: - body: - persistent: - repositories.default_repository: null - --- "Cannot delete default repository": # Set first repo as default @@ -90,13 +86,6 @@ teardown: - is_true: test_repo_1 - # Clean up: clear the default setting - - do: - cluster.put_settings: - body: - persistent: - repositories.default_repository: null - --- "Cannot set non-existent repository as default": # Try to set a non-existent repository as default @@ -150,13 +139,6 @@ teardown: - match: { persistent.repositories.default_repository: "test_repo_2" } - # Clean up: clear the default setting - - do: - cluster.put_settings: - body: - persistent: - repositories.default_repository: null - --- "Clear default repository setting": # Set a repository as default diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java index d6d002377eebf..06808013f36a2 100644 --- a/server/src/main/java/module-info.java +++ b/server/src/main/java/module-info.java @@ -427,7 +427,6 @@ with org.elasticsearch.action.bulk.BulkFeatures, org.elasticsearch.features.InfrastructureFeatures, - org.elasticsearch.repositories.RepositoriesFeatures, org.elasticsearch.rest.action.admin.cluster.ClusterRerouteFeatures, org.elasticsearch.rest.action.admin.cluster.GetSnapshotsFeatures, org.elasticsearch.index.mapper.MapperFeatures, diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesFeatures.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesFeatures.java deleted file mode 100644 index ca72c84ac5e9f..0000000000000 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesFeatures.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.repositories; - -import org.elasticsearch.features.FeatureSpecification; -import org.elasticsearch.features.NodeFeature; - -import java.util.Set; - -/** - * Provides features for repositories functionality. - */ -public class RepositoriesFeatures implements FeatureSpecification { - - /** - * Feature for the default repository cluster setting. - * Introduced in 9.2.0 to allow setting a default repository for snapshot and restore operations. - */ - public static final NodeFeature DEFAULT_REPOSITORY_SETTING = new NodeFeature("repositories.default_repository_setting"); - - @Override - public Set getFeatures() { - return Set.of(DEFAULT_REPOSITORY_SETTING); - } -} diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java index cd66b1113f358..442fe4dde39ef 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java @@ -189,6 +189,7 @@ public String getDefaultRepository() { */ private void setDefaultRepository(String repositoryName) { this.defaultRepository = repositoryName; + logger.info("Default repository set to [{}]", repositoryName); } /** @@ -213,7 +214,6 @@ private void validateDefaultRepository(String repositoryName) { + repositoryName ); } - logger.info("Default repository set to [{}]", repositoryName); } /** diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterUpdateSettingsAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterUpdateSettingsAction.java index 112d34136bc99..6a8c406d47a2c 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterUpdateSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterUpdateSettingsAction.java @@ -33,6 +33,9 @@ public class RestClusterUpdateSettingsAction extends BaseRestHandler { private static final String PERSISTENT = "persistent"; private static final String TRANSIENT = "transient"; + public static final String SUPPORTS_DEFAULT_REPO_SETTING = "repositories.default_repository_setting"; + private static final Set CAPABILITIES = Set.of(SUPPORTS_DEFAULT_REPO_SETTING); + @Override public List routes() { return List.of(new Route(PUT, "/_cluster/settings")); @@ -74,4 +77,8 @@ public boolean canTripCircuitBreaker() { return false; } + @Override + public Set supportedCapabilities() { + return CAPABILITIES; + } } diff --git a/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification b/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification index 35a4d0ce229d4..42bf3c942daaf 100644 --- a/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification +++ b/server/src/main/resources/META-INF/services/org.elasticsearch.features.FeatureSpecification @@ -9,7 +9,6 @@ org.elasticsearch.action.bulk.BulkFeatures org.elasticsearch.features.InfrastructureFeatures -org.elasticsearch.repositories.RepositoriesFeatures org.elasticsearch.rest.action.admin.cluster.ClusterRerouteFeatures org.elasticsearch.rest.action.admin.cluster.GetSnapshotsFeatures org.elasticsearch.index.IndexFeatures From f8f756bf053c74e9e1a630043c075c561b6dd072 Mon Sep 17 00:00:00 2001 From: Sean Zatz Date: Wed, 17 Dec 2025 14:08:40 -0500 Subject: [PATCH 13/14] only validate on master update thread. Also, prevent default repo from being read-only --- .../repositories/10_default_repository.yml | 142 ++++++++++++++++++ .../repositories/RepositoriesService.java | 92 +++++++----- 2 files changed, 200 insertions(+), 34 deletions(-) diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/repositories/10_default_repository.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/repositories/10_default_repository.yml index 4f7d4d8ce380b..6bc06d778fd0c 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/repositories/10_default_repository.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/repositories/10_default_repository.yml @@ -170,3 +170,145 @@ teardown: - match: { acknowledged: true } +--- +"Cannot set read-only repository as default": + # Create a read-only repository + - do: + snapshot.create_repository: + repository: readonly_repo + verify: false + body: + type: fs + settings: + location: "readonly_repo_loc" + readonly: true + + # Try to set read-only repository as default - should fail + - do: + catch: /Repository \[readonly_repo\] is marked as read-only/ + cluster.put_settings: + body: + persistent: + repositories.default_repository: "readonly_repo" + + # Clean up + - do: + snapshot.delete_repository: + repository: readonly_repo + +--- +"Cannot mark default repository as read-only": + # Set first repo as default + - do: + cluster.put_settings: + body: + persistent: + repositories.default_repository: "test_repo_1" + + # Try to update the default repository to be read-only - should fail + - do: + catch: /Repository \[test_repo_1\] is the default repository/ + snapshot.create_repository: + repository: test_repo_1 + verify: false + body: + type: fs + settings: + location: "test_repo_1_loc" + readonly: true + + # Verify repository is still not read-only + - do: + snapshot.get_repository: + repository: test_repo_1 + + - is_false: test_repo_1.settings.readonly + +--- +"Cannot create default repository as read-only": + # First, set test_repo_1 as default + - do: + cluster.put_settings: + body: + persistent: + repositories.default_repository: "test_repo_1" + + # Try to create a new repository (test_repo_3) as read-only and then set it as default + - do: + snapshot.create_repository: + repository: test_repo_3 + verify: false + body: + type: fs + settings: + location: "test_repo_3_loc" + readonly: true + + # Now try to set the read-only repository as default - should fail + - do: + catch: /Repository \[test_repo_3\] is marked as read-only/ + cluster.put_settings: + body: + persistent: + repositories.default_repository: "test_repo_3" + + # Clean up + - do: + snapshot.delete_repository: + repository: test_repo_3 + + # Clear the default setting + - do: + cluster.put_settings: + body: + persistent: + repositories.default_repository: null + +--- +"Can mark repository as read-only after removing default setting": + # Set first repo as default + - do: + cluster.put_settings: + body: + persistent: + repositories.default_repository: "test_repo_1" + + # Cannot mark it as read-only while it's the default + - do: + catch: /Repository \[test_repo_1\] is the default repository/ + snapshot.create_repository: + repository: test_repo_1 + verify: false + body: + type: fs + settings: + location: "test_repo_1_loc" + readonly: true + + # Clear the default setting + - do: + cluster.put_settings: + body: + persistent: + repositories.default_repository: null + + # Now we can mark it as read-only + - do: + snapshot.create_repository: + repository: test_repo_1 + verify: false + body: + type: fs + settings: + location: "test_repo_1_loc" + readonly: true + + - match: { acknowledged: true } + + # Verify repository is now read-only + - do: + snapshot.get_repository: + repository: test_repo_1 + + - match: { test_repo_1.settings.readonly: "true" } + diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java index 442fe4dde39ef..f07eddb1e81b5 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java @@ -37,6 +37,7 @@ import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodeRole; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.cluster.service.MasterService; import org.elasticsearch.common.Strings; import org.elasticsearch.common.component.AbstractLifecycleComponent; import org.elasticsearch.common.regex.Regex; @@ -193,19 +194,28 @@ private void setDefaultRepository(String repositoryName) { } /** - * Validates that the default repository is registered. + * Validates that the default repository is registered and not read-only. * This validator is called when the default repository setting is updated. * Empty strings are allowed to clear the default repository setting. * + * Only runs on master node/master update thread to avoid issues at node start time + * * @param repositoryName the repository name to validate - * @throws IllegalArgumentException if the repository name is not empty and the repository doesn't exist + * @throws IllegalArgumentException if the repository name is not empty and the repository doesn't exist or is not empty and read-only */ private void validateDefaultRepository(String repositoryName) { + // Only validate on the master update thread to avoid validation issues at node start time + // when repositories may not be initialized yet. + if (MasterService.isMasterUpdateThread() == false) { + logger.debug("Skipping validation of default repository [{}] outside master update thread", repositoryName); + return; + } if (Strings.isEmpty(repositoryName)) { logger.info("Default repository cleared"); return; } - if (isRepositoryRegistered(repositoryName) == false) { + Repository repository = repositoryOrNull(ProjectId.DEFAULT, repositoryName); + if (repository == null) { throw new IllegalArgumentException( "Repository [" + repositoryName @@ -214,24 +224,15 @@ private void validateDefaultRepository(String repositoryName) { + repositoryName ); } - } - /** - * Checks if a repository is registered in the default project, any other project, or internal repositories. - * - * @param repositoryName the repository name to check - * @return true if the repository is registered, false otherwise - */ - private boolean isRepositoryRegistered(String repositoryName) { - if (repositoryOrNull(ProjectId.DEFAULT, repositoryName) != null) { - return true; - } - boolean found = repositories.values().stream().anyMatch(projectRepos -> projectRepos.containsKey(repositoryName)); - if (found) { - return true; + if (isReadOnly(repository.getMetadata().settings())) { + throw new IllegalArgumentException( + "Repository [" + + repositoryName + + "] is marked as read-only. " + + "Cannot set a read-only repository as the default repository." + ); } - found = internalRepositories.values().stream().anyMatch(projectRepos -> projectRepos.containsKey(repositoryName)); - return found; } /** @@ -392,7 +393,7 @@ public ClusterState execute(ClusterState currentState) { List repositoriesMetadata = new ArrayList<>(repositories.repositories().size() + 1); for (RepositoryMetadata repositoryMetadata : repositories.repositories()) { if (repositoryMetadata.name().equals(request.name())) { - rejectInvalidReadonlyFlagChange(repositoryMetadata, request.settings()); + rejectInvalidReadonlyFlagChange(repositoryMetadata, request.settings(), repositoriesService.getDefaultRepository()); final RepositoryMetadata newRepositoryMetadata = new RepositoryMetadata( request.name(), // Copy the UUID from the existing instance rather than resetting it back to MISSING_UUID which would force us to @@ -441,6 +442,16 @@ public ClusterState execute(ClusterState currentState) { } } if (found == false) { + // Check if creating a read-only repository that is the default repository + if (isReadOnly(request.settings()) && request.name().equals(repositoriesService.getDefaultRepository())) { + throw new IllegalArgumentException( + "Repository [" + + request.name() + + "] is the default repository. " + + "Cannot create the default repository as read-only. " + + "Please change the default repository setting before creating this repository as read-only." + ); + } repositoriesMetadata.add(new RepositoryMetadata(request.name(), request.type(), request.settings())); } repositories = new RepositoriesMetadata(repositoriesMetadata); @@ -1254,21 +1265,34 @@ private static boolean assertReadonlyRepositoriesNotInUseForWrites(ProjectState /** * Reject a change to the {@code readonly} setting if there is a pending generation change in progress, i.e. some node somewhere is - * updating the root {@link RepositoryData} blob. + * updating the root {@link RepositoryData} blob, or if the repository is the default repository. */ - private static void rejectInvalidReadonlyFlagChange(RepositoryMetadata existingRepositoryMetadata, Settings newSettings) { - if (isReadOnly(newSettings) - && isReadOnly(existingRepositoryMetadata.settings()) == false - && existingRepositoryMetadata.generation() >= RepositoryData.EMPTY_REPO_GEN - && existingRepositoryMetadata.generation() != existingRepositoryMetadata.pendingGeneration()) { - throw newRepositoryConflictException( - existingRepositoryMetadata.name(), - Strings.format( - "currently updating root blob generation from [%d] to [%d], cannot update readonly flag", - existingRepositoryMetadata.generation(), - existingRepositoryMetadata.pendingGeneration() - ) - ); + private static void rejectInvalidReadonlyFlagChange( + RepositoryMetadata existingRepositoryMetadata, + Settings newSettings, + String defaultRepository + ) { + if (isReadOnly(newSettings) && isReadOnly(existingRepositoryMetadata.settings()) == false) { + if (existingRepositoryMetadata.name().equals(defaultRepository)) { + throw new IllegalArgumentException( + "Repository [" + + existingRepositoryMetadata.name() + + "] is the default repository. " + + "Cannot mark the default repository as read-only. " + + "Please change the default repository setting before marking this repository as read-only." + ); + } + if (existingRepositoryMetadata.generation() >= RepositoryData.EMPTY_REPO_GEN + && existingRepositoryMetadata.generation() != existingRepositoryMetadata.pendingGeneration()) { + throw newRepositoryConflictException( + existingRepositoryMetadata.name(), + Strings.format( + "currently updating root blob generation from [%d] to [%d], cannot update readonly flag", + existingRepositoryMetadata.generation(), + existingRepositoryMetadata.pendingGeneration() + ) + ); + } } } From 7155adc95584d028d6c9a8b04c1433edd11e0cef Mon Sep 17 00:00:00 2001 From: Sean Zatz Date: Wed, 17 Dec 2025 23:56:13 -0500 Subject: [PATCH 14/14] Only use default repo --- .../org/elasticsearch/repositories/RepositoriesService.java | 2 +- .../core-rest-tests-with-multiple-projects/build.gradle | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java index f07eddb1e81b5..51267cf996c88 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java @@ -214,7 +214,7 @@ private void validateDefaultRepository(String repositoryName) { logger.info("Default repository cleared"); return; } - Repository repository = repositoryOrNull(ProjectId.DEFAULT, repositoryName); + Repository repository = repositories.getOrDefault(ProjectId.DEFAULT, Map.of()).get(repositoryName); if (repository == null) { throw new IllegalArgumentException( "Repository [" diff --git a/x-pack/qa/multi-project/core-rest-tests-with-multiple-projects/build.gradle b/x-pack/qa/multi-project/core-rest-tests-with-multiple-projects/build.gradle index 6684a94b8a02e..852ab156997f1 100644 --- a/x-pack/qa/multi-project/core-rest-tests-with-multiple-projects/build.gradle +++ b/x-pack/qa/multi-project/core-rest-tests-with-multiple-projects/build.gradle @@ -76,6 +76,9 @@ tasks.named("yamlRestTest").configure { // Linked project configuration via ClusterSettings currently does not support multi-project '^cluster.stats/30_ccs_stats/cross-cluster search stats search', + + // Default Repo via ClusterSettings are not currently supported in multi-project + '^repositories/10_default_repository/*', ]; if (buildParams.snapshotBuild == false) { blacklist += [];