Skip to content
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
---
setup:
- requires:
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:
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: {}

- match: { persistent.repositories.default_repository: "test_repo_1" }

---
"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

---
"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" }

---
"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 }

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -589,6 +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,
RepositoriesService.DEFAULT_REPOSITORY_SETTING,
RestoreService.REFRESH_REPO_UUID_ON_RESTORE_SETTING,
FsHealthService.ENABLED_SETTING,
FsHealthService.REFRESH_INTERVAL_SETTING,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, some snapshot and restore operations may require an explicit repository name.
*/
public static final Setting<String> DEFAULT_REPOSITORY_SETTING = Setting.simpleString(
"repositories.default_repository",
Setting.Property.NodeScope,
Setting.Property.Dynamic
);

private final Map<String, Repository.Factory> typesRegistry;
private final Map<String, Repository.Factory> internalTypesRegistry;

Expand All @@ -125,6 +135,8 @@ public class RepositoriesService extends AbstractLifecycleComponent implements C

private final List<BiConsumer<Snapshot, IndexVersion>> preRestoreChecks;

private volatile String defaultRepository;

@SuppressWarnings("this-escape")
public RepositoriesService(
Settings settings,
Expand Down Expand Up @@ -154,9 +166,74 @@ public RepositoriesService(
threadPool.relativeTimeInMillisSupplier()
);
this.preRestoreChecks = preRestoreChecks;
this.defaultRepository = DEFAULT_REPOSITORY_SETTING.get(settings);
clusterService.getClusterSettings()
.addSettingsUpdateConsumer(DEFAULT_REPOSITORY_SETTING, this::setDefaultRepository, this::validateDefaultRepository);
Comment on lines +170 to +171
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should get someone from the Distributed team to have a look at this validation, to verify if there's any reason this validation could cause issues. I'll add the area label to the PR, could you reach out to #es-destrib and ask for a review?

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;
logger.info("Default repository set to [{}]", repositoryName);
}

/**
* 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.
*
* @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) {
if (Strings.isEmpty(repositoryName)) {
logger.info("Default repository cleared");
return;
}
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
);
}
}

/**
* 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;
}
Comment on lines +225 to +235
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two comments:

  1. We should not check internal repositories since those are used for CCR.
  2. We should be explicit about it being the default project and not check with any other project


/**
* Registers new repository in the cluster
* <p>
Expand Down Expand Up @@ -568,6 +645,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;
Expand Down Expand Up @@ -1135,6 +1213,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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> CAPABILITIES = Set.of(SUPPORTS_DEFAULT_REPO_SETTING);

@Override
public List<Route> routes() {
return List.of(new Route(PUT, "/_cluster/settings"));
Expand Down Expand Up @@ -74,4 +77,8 @@ public boolean canTripCircuitBreaker() {
return false;
}

@Override
public Set<String> supportedCapabilities() {
return CAPABILITIES;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Loading