Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract repository-resolution logic #105760

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,20 @@
import org.elasticsearch.cluster.block.ClusterBlockLevel;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.cluster.metadata.RepositoriesMetadata;
import org.elasticsearch.cluster.metadata.RepositoryMetadata;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.regex.Regex;
import org.elasticsearch.common.util.concurrent.EsExecutors;
import org.elasticsearch.repositories.RepositoryMissingException;
import org.elasticsearch.repositories.ResolvedRepositories;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

/**
* Transport action for get repositories operation
*/
public class TransportGetRepositoriesAction extends TransportMasterNodeReadAction<GetRepositoriesRequest, GetRepositoriesResponse> {

public static final String ALL_PATTERN = "_all";

@Inject
public TransportGetRepositoriesAction(
TransportService transportService,
Expand All @@ -60,11 +51,6 @@ public TransportGetRepositoriesAction(
);
}

public static boolean isMatchAll(String[] patterns) {
return (patterns.length == 0)
|| (patterns.length == 1 && (ALL_PATTERN.equalsIgnoreCase(patterns[0]) || Regex.isMatchAllPattern(patterns[0])));
}

@Override
protected ClusterBlockException checkBlock(GetRepositoriesRequest request, ClusterState state) {
return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_READ);
Expand All @@ -77,69 +63,11 @@ protected void masterOperation(
ClusterState state,
final ActionListener<GetRepositoriesResponse> listener
) {
RepositoriesResult result = getRepositories(state, request.repositories());
final var result = ResolvedRepositories.resolve(state, request.repositories());
if (result.hasMissingRepositories()) {
listener.onFailure(new RepositoryMissingException(String.join(", ", result.missing())));
} else {
listener.onResponse(new GetRepositoriesResponse(new RepositoriesMetadata(result.metadata)));
}
}

/**
* Get repository metadata for given repository names from given cluster state.
*
* @param state Cluster state
* @param repoNames Repository names or patterns to get metadata for
* @return a result with the repository metadata that were found in the cluster state and the missing repositories
*/
public static RepositoriesResult getRepositories(ClusterState state, String[] repoNames) {
RepositoriesMetadata repositories = RepositoriesMetadata.get(state);
if (isMatchAll(repoNames)) {
return new RepositoriesResult(repositories.repositories());
}
final List<String> missingRepositories = new ArrayList<>();
final List<String> includePatterns = new ArrayList<>();
final List<String> excludePatterns = new ArrayList<>();
boolean seenWildcard = false;
for (String repositoryOrPattern : repoNames) {
if (seenWildcard && repositoryOrPattern.length() > 1 && repositoryOrPattern.startsWith("-")) {
excludePatterns.add(repositoryOrPattern.substring(1));
} else {
if (Regex.isSimpleMatchPattern(repositoryOrPattern)) {
seenWildcard = true;
} else {
if (repositories.repository(repositoryOrPattern) == null) {
missingRepositories.add(repositoryOrPattern);
}
}
includePatterns.add(repositoryOrPattern);
}
}
final String[] excludes = excludePatterns.toArray(Strings.EMPTY_ARRAY);
final Set<RepositoryMetadata> repositoryListBuilder = new LinkedHashSet<>(); // to keep insertion order
for (String repositoryOrPattern : includePatterns) {
for (RepositoryMetadata repository : repositories.repositories()) {
if (repositoryListBuilder.contains(repository) == false
&& Regex.simpleMatch(repositoryOrPattern, repository.name())
&& Regex.simpleMatch(excludes, repository.name()) == false) {
repositoryListBuilder.add(repository);
}
}
}
return new RepositoriesResult(List.copyOf(repositoryListBuilder), missingRepositories);
}

/**
* A holder class that consists of the repository metadata and the names of the repositories that were not found in the cluster state.
*/
public record RepositoriesResult(List<RepositoryMetadata> metadata, List<String> missing) {

RepositoriesResult(List<RepositoryMetadata> repositoryMetadata) {
this(repositoryMetadata, List.of());
}

boolean hasMissingRepositories() {
return missing.isEmpty() == false;
listener.onResponse(new GetRepositoriesResponse(new RepositoriesMetadata(result.repositoryMetadata())));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.admin.cluster.repositories.get.TransportGetRepositoriesAction;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.RefCountingListener;
import org.elasticsearch.action.support.master.TransportMasterNodeAction;
Expand All @@ -33,6 +32,7 @@
import org.elasticsearch.repositories.Repository;
import org.elasticsearch.repositories.RepositoryData;
import org.elasticsearch.repositories.RepositoryMissingException;
import org.elasticsearch.repositories.ResolvedRepositories;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.snapshots.Snapshot;
import org.elasticsearch.snapshots.SnapshotId;
Expand Down Expand Up @@ -111,7 +111,7 @@ protected void masterOperation(

new GetSnapshotsOperation(
(CancellableTask) task,
TransportGetRepositoriesAction.getRepositories(state, request.repositories()),
ResolvedRepositories.resolve(state, request.repositories()),
request.isSingleRepositoryRequest() == false,
request.snapshots(),
request.ignoreUnavailable(),
Expand Down Expand Up @@ -172,7 +172,7 @@ private class GetSnapshotsOperation {

GetSnapshotsOperation(
CancellableTask cancellableTask,
TransportGetRepositoriesAction.RepositoriesResult repositoriesResult,
ResolvedRepositories resolvedRepositories,
boolean isMultiRepoRequest,
String[] snapshots,
boolean ignoreUnavailable,
Expand All @@ -188,7 +188,7 @@ private class GetSnapshotsOperation {
boolean indices
) {
this.cancellableTask = cancellableTask;
this.repositories = repositoriesResult.metadata();
this.repositories = resolvedRepositories.repositoryMetadata();
this.isMultiRepoRequest = isMultiRepoRequest;
this.snapshots = snapshots;
this.ignoreUnavailable = ignoreUnavailable;
Expand All @@ -203,7 +203,7 @@ private class GetSnapshotsOperation {
this.verbose = verbose;
this.indices = indices;

for (final var missingRepo : repositoriesResult.missing()) {
for (final var missingRepo : resolvedRepositories.missing()) {
failuresByRepository.put(missingRepo, new RepositoryMissingException(missingRepo));
}
}
Expand Down Expand Up @@ -326,7 +326,7 @@ private void loadSnapshotInfos(
}

final Set<Snapshot> toResolve = new HashSet<>();
if (TransportGetRepositoriesAction.isMatchAll(snapshots)) {
if (ResolvedRepositories.isMatchAll(snapshots)) {
toResolve.addAll(allSnapshotIds.values());
} else {
final List<String> includePatterns = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/

package org.elasticsearch.repositories;

import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.metadata.RepositoriesMetadata;
import org.elasticsearch.cluster.metadata.RepositoryMetadata;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.regex.Regex;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

/**
* The result of calling {@link #resolve(ClusterState, String[])} to resolve a description of some snapshot repositories (from a path
* component of a request to the get-repositories or get-snapshots APIs) against the known repositories in the cluster state: the
* {@link RepositoryMetadata} for the extant repositories that match the description, together with a list of the parts of the description
* that failed to match any known repository.
*
* @param repositoryMetadata The {@link RepositoryMetadata} for the repositories that matched the description.
* @param missing The parts of the description which matched no repositories.
*/
public record ResolvedRepositories(List<RepositoryMetadata> repositoryMetadata, List<String> missing) {

public static final String ALL_PATTERN = "_all";

public static boolean isMatchAll(String[] patterns) {
return patterns.length == 0
|| (patterns.length == 1 && (ALL_PATTERN.equalsIgnoreCase(patterns[0]) || Regex.isMatchAllPattern(patterns[0])));
}

public static ResolvedRepositories resolve(ClusterState state, String[] patterns) {
final var repositories = RepositoriesMetadata.get(state);
if (isMatchAll(patterns)) {
return new ResolvedRepositories(repositories.repositories(), List.of());
}

final List<String> missingRepositories = new ArrayList<>();
final List<String> includePatterns = new ArrayList<>();
final List<String> excludePatterns = new ArrayList<>();
boolean seenWildcard = false;
for (final var pattern : patterns) {
if (seenWildcard && pattern.length() > 1 && pattern.startsWith("-")) {
excludePatterns.add(pattern.substring(1));
} else {
if (Regex.isSimpleMatchPattern(pattern)) {
seenWildcard = true;
} else {
if (repositories.repository(pattern) == null) {
missingRepositories.add(pattern);
}
}
includePatterns.add(pattern);
}
}
final var excludes = excludePatterns.toArray(Strings.EMPTY_ARRAY);
final Set<RepositoryMetadata> repositoryListBuilder = new LinkedHashSet<>(); // to keep insertion order
for (String repositoryOrPattern : includePatterns) {
for (RepositoryMetadata repository : repositories.repositories()) {
if (repositoryListBuilder.contains(repository) == false
&& Regex.simpleMatch(repositoryOrPattern, repository.name())
&& Regex.simpleMatch(excludes, repository.name()) == false) {
repositoryListBuilder.add(repository);
}
}
}
return new ResolvedRepositories(List.copyOf(repositoryListBuilder), missingRepositories);
}

public boolean hasMissingRepositories() {
return missing.isEmpty() == false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
package org.elasticsearch.rest.action.cat;

import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.admin.cluster.repositories.get.TransportGetRepositoriesAction;
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest;
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse;
import org.elasticsearch.client.internal.node.NodeClient;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.Table;
import org.elasticsearch.common.time.DateFormatter;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.repositories.ResolvedRepositories;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.rest.Scope;
Expand Down Expand Up @@ -50,7 +50,7 @@ public String getName() {

@Override
protected RestChannelConsumer doCatRequest(final RestRequest request, NodeClient client) {
final String[] matchAll = { TransportGetRepositoriesAction.ALL_PATTERN };
final String[] matchAll = { ResolvedRepositories.ALL_PATTERN };
GetSnapshotsRequest getSnapshotsRequest = new GetSnapshotsRequest().repositories(request.paramAsStringArray("repository", matchAll))
.snapshots(matchAll);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/

package org.elasticsearch.repositories;

import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.metadata.RepositoriesMetadata;
import org.elasticsearch.cluster.metadata.RepositoryMetadata;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.test.ESTestCase;
import org.hamcrest.Matchers;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class ResolvedRepositoriesTests extends ESTestCase {

public void testAll() {
runMatchAllTest();
runMatchAllTest("*");
runMatchAllTest("_all");
}

private static void runMatchAllTest(String... patterns) {
final var state = clusterStateWithRepositories(randomList(1, 4, ESTestCase::randomIdentifier).toArray(String[]::new));
final var result = getRepositories(state, patterns);
assertEquals(RepositoriesMetadata.get(state).repositories(), result.repositoryMetadata());
assertThat(result.missing(), Matchers.empty());
assertFalse(result.hasMissingRepositories());
}

public void testMatchingName() {
final var state = clusterStateWithRepositories(randomList(1, 4, ESTestCase::randomIdentifier).toArray(String[]::new));
final var name = randomFrom(RepositoriesMetadata.get(state).repositories()).name();
final var result = getRepositories(state, name);
assertEquals(List.of(RepositoriesMetadata.get(state).repository(name)), result.repositoryMetadata());
assertThat(result.missing(), Matchers.empty());
assertFalse(result.hasMissingRepositories());
}

public void testMismatchingName() {
final var state = clusterStateWithRepositories(randomList(1, 4, ESTestCase::randomIdentifier).toArray(String[]::new));
final var notAName = randomValueOtherThanMany(
n -> RepositoriesMetadata.get(state).repositories().stream().anyMatch(m -> n.equals(m.name())),
ESTestCase::randomIdentifier
);
final var result = getRepositories(state, notAName);
assertEquals(List.of(), result.repositoryMetadata());
assertEquals(List.of(notAName), result.missing());
assertTrue(result.hasMissingRepositories());
}

public void testWildcards() {
final var state = clusterStateWithRepositories("test-match-1", "test-match-2", "test-exclude", "other-repo");

runWildcardTest(state, List.of("test-match-1", "test-match-2", "test-exclude"), "test-*");
runWildcardTest(state, List.of("test-match-1", "test-match-2"), "test-*1", "test-*2");
runWildcardTest(state, List.of("test-match-2", "test-match-1"), "test-*2", "test-*1");
runWildcardTest(state, List.of("test-match-1", "test-match-2"), "test-*", "-*-exclude");
runWildcardTest(state, List.of(), "no-*-repositories");
runWildcardTest(state, List.of("test-match-1", "test-match-2", "other-repo"), "test-*", "-*-exclude", "other-repo");
runWildcardTest(state, List.of("other-repo", "test-match-1", "test-match-2"), "other-repo", "test-*", "-*-exclude");
}

private static void runWildcardTest(ClusterState clusterState, List<String> expectedNames, String... patterns) {
final var result = getRepositories(clusterState, patterns);
final var description = Strings.format("%s should yield %s", Arrays.toString(patterns), expectedNames);
assertFalse(description, result.hasMissingRepositories());
assertEquals(description, expectedNames, result.repositoryMetadata().stream().map(RepositoryMetadata::name).toList());
}

private static ResolvedRepositories getRepositories(ClusterState clusterState, String... patterns) {
return ResolvedRepositories.resolve(clusterState, patterns);
}

private static ClusterState clusterStateWithRepositories(String... repoNames) {
final var repositories = new ArrayList<RepositoryMetadata>(repoNames.length);
for (final var repoName : repoNames) {
repositories.add(new RepositoryMetadata(repoName, "test", Settings.EMPTY));
}
return ClusterState.EMPTY_STATE.copyAndUpdateMetadata(
b -> b.putCustom(RepositoriesMetadata.TYPE, new RepositoriesMetadata(repositories))
);
}

}
Loading