This is an automated email from the ASF dual-hosted git repository.
gerlowskija pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/main by this push:
new fe4f669a3a1 SOLR-16394: Migrate coll backup creation to JAX-RS (#1541)
fe4f669a3a1 is described below
commit fe4f669a3a193b91533a3f8aa1650b10da0a0b3f
Author: Jason Gerlowski <[email protected]>
AuthorDate: Thu Apr 6 16:00:21 2023 -0400
SOLR-16394: Migrate coll backup creation to JAX-RS (#1541)
This commit makes various cosmetic improvements to Solr's v2
(collection-level) create backup API, to bring it more into line with
the more REST-ful v2 design.
As of this commit, the create-backup API is now:
- POST /api/collections/collName/backups/backName/versions
It also migrates the API definition to JAX-RS.
---
solr/CHANGES.txt | 3 +
.../cloud/api/collections/DeleteBackupCmd.java | 3 +-
.../org/apache/solr/handler/CollectionsAPI.java | 11 -
.../solr/handler/admin/CollectionsHandler.java | 87 +------
.../solr/handler/admin/api/AdminAPIBase.java | 23 ++
.../admin/api/CreateCollectionBackupAPI.java | 267 +++++++++++++++++++++
.../org/apache/solr/jersey/SolrJacksonMapper.java | 11 +
.../apache/solr/handler/V2ApiIntegrationTest.java | 8 +-
.../handler/admin/V2CollectionsAPIMappingTest.java | 30 ---
.../admin/api/V2CollectionBackupApiTest.java | 100 ++++++++
.../pages/collection-management.adoc | 35 ++-
.../collections/AbstractIncrementalBackupTest.java | 4 +-
12 files changed, 441 insertions(+), 141 deletions(-)
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 11b8566c883..ced2d58539e 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -71,6 +71,9 @@ Improvements
available under the `PUT` and `DELETE` verbs at
`/api/collections/collName/properties/propName` depending on whether the
property is
being upserted or deleted. (Jason Gerlowski)
+* SOLR-16394: The path of the v2 "collection backup" API has been tweaked
slightly to be more intuitive, and is now available at
+ `POST /api/collections/backups/backupName/versions`. (Jason Gerlowski)
+
Optimizations
---------------------
diff --git
a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteBackupCmd.java
b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteBackupCmd.java
index d930f69f583..72e1043a957 100644
---
a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteBackupCmd.java
+++
b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteBackupCmd.java
@@ -40,6 +40,7 @@ import org.apache.solr.common.cloud.ClusterState;
import org.apache.solr.common.cloud.ZkNodeProps;
import org.apache.solr.common.params.CoreAdminParams;
import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.backup.AggregateBackupStats;
import org.apache.solr.core.backup.BackupFilePaths;
@@ -237,7 +238,7 @@ public class DeleteBackupCmd implements
CollApiCmds.CollectionApiCommand {
List<NamedList<Object>> shardBackupIdDetails = new ArrayList<>();
results.add("deleted", shardBackupIdDetails);
for (BackupId backupId : backupIdDeletes) {
- NamedList<Object> backupIdResult = new NamedList<>();
+ NamedList<Object> backupIdResult = new SimpleOrderedMap<>();
try {
BackupProperties props =
diff --git a/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
b/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
index a07ef69ca9f..ac3c49d7a90 100644
--- a/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
@@ -35,7 +35,6 @@ import java.util.stream.Collectors;
import org.apache.solr.api.Command;
import org.apache.solr.api.EndPoint;
import org.apache.solr.api.PayloadObj;
-import org.apache.solr.client.solrj.request.beans.BackupCollectionPayload;
import org.apache.solr.client.solrj.request.beans.CreateAliasPayload;
import org.apache.solr.client.solrj.request.beans.CreatePayload;
import org.apache.solr.client.solrj.request.beans.RestoreCollectionPayload;
@@ -48,7 +47,6 @@ import org.apache.solr.handler.admin.CollectionsHandler;
public class CollectionsAPI {
public static final String V2_CREATE_COLLECTION_CMD = "create";
- public static final String V2_BACKUP_CMD = "backup-collection";
public static final String V2_RESTORE_CMD = "restore-collection";
public static final String V2_CREATE_ALIAS_CMD = "create-alias";
@@ -66,15 +64,6 @@ public class CollectionsAPI {
permission = COLL_EDIT_PERM)
public class CollectionsCommands {
- @Command(name = V2_BACKUP_CMD)
- public void backupCollection(PayloadObj<BackupCollectionPayload> obj)
throws Exception {
- final Map<String, Object> v1Params = obj.get().toMap(new HashMap<>());
- v1Params.put(ACTION, CollectionAction.BACKUP.toLower());
-
- collectionsHandler.handleRequestBody(
- wrapParams(obj.getRequest(), v1Params), obj.getResponse());
- }
-
@Command(name = V2_RESTORE_CMD)
@SuppressWarnings("unchecked")
public void restoreBackup(PayloadObj<RestoreCollectionPayload> obj) throws
Exception {
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
index 67ae42ca38a..3bbbfa2c687 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
@@ -212,6 +212,7 @@ import org.apache.solr.handler.admin.api.AliasPropertyAPI;
import org.apache.solr.handler.admin.api.BalanceShardUniqueAPI;
import org.apache.solr.handler.admin.api.CollectionPropertyAPI;
import org.apache.solr.handler.admin.api.CollectionStatusAPI;
+import org.apache.solr.handler.admin.api.CreateCollectionBackupAPI;
import org.apache.solr.handler.admin.api.CreateShardAPI;
import org.apache.solr.handler.admin.api.DeleteAliasAPI;
import org.apache.solr.handler.admin.api.DeleteCollectionAPI;
@@ -1389,87 +1390,10 @@ public class CollectionsHandler extends
RequestHandlerBase implements Permission
BACKUP_OP(
BACKUP,
(req, rsp, h) -> {
- req.getParams().required().check(NAME, COLLECTION_PROP);
-
- final String extCollectionName =
req.getParams().get(COLLECTION_PROP);
- final boolean followAliases =
req.getParams().getBool(FOLLOW_ALIASES, false);
- final String collectionName =
- followAliases
- ? h.coreContainer
- .getZkController()
- .getZkStateReader()
- .getAliases()
- .resolveSimpleAlias(extCollectionName)
- : extCollectionName;
- final ClusterState clusterState =
h.coreContainer.getZkController().getClusterState();
- if (!clusterState.hasCollection(collectionName)) {
- throw new SolrException(
- ErrorCode.BAD_REQUEST,
- "Collection '" + collectionName + "' does not exist, no action
taken.");
- }
-
- CoreContainer cc = h.coreContainer;
- String repo = req.getParams().get(CoreAdminParams.BACKUP_REPOSITORY);
- BackupRepository repository = cc.newBackupRepository(repo);
-
- String location =
-
repository.getBackupLocation(req.getParams().get(CoreAdminParams.BACKUP_LOCATION));
- if (location == null) {
- // Refresh the cluster property file to make sure the value set
for location is the
- // latest. Check if the location is specified in the cluster
property.
- location =
- new
ClusterProperties(h.coreContainer.getZkController().getZkClient())
- .getClusterProperty(CoreAdminParams.BACKUP_LOCATION, null);
- if (location == null) {
- throw new SolrException(
- ErrorCode.BAD_REQUEST,
- "'location' is not specified as a query"
- + " parameter or as a default repository property or as
a cluster property.");
- }
- }
- boolean incremental =
req.getParams().getBool(CoreAdminParams.BACKUP_INCREMENTAL, true);
-
- // Check if the specified location is valid for this repository.
- final URI uri = repository.createDirectoryURI(location);
- try {
- if (!repository.exists(uri)) {
- throw new SolrException(
- ErrorCode.SERVER_ERROR, "specified location " + uri + " does
not exist.");
- }
- } catch (IOException ex) {
- throw new SolrException(
- ErrorCode.SERVER_ERROR,
- "Failed to check the existence of " + uri + ". Is it valid?",
- ex);
- }
-
- String strategy =
- req.getParams()
- .get(
- CollectionAdminParams.INDEX_BACKUP_STRATEGY,
- CollectionAdminParams.COPY_FILES_STRATEGY);
- if
(!CollectionAdminParams.INDEX_BACKUP_STRATEGIES.contains(strategy)) {
- throw new SolrException(
- ErrorCode.BAD_REQUEST, "Unknown index backup strategy " +
strategy);
- }
-
- Map<String, Object> params =
- copy(
- req.getParams(),
- null,
- NAME,
- COLLECTION_PROP,
- FOLLOW_ALIASES,
- CoreAdminParams.COMMIT_NAME,
- CoreAdminParams.MAX_NUM_BACKUP_POINTS);
- params.put(CoreAdminParams.BACKUP_LOCATION, location);
- if (repo != null) {
- params.put(CoreAdminParams.BACKUP_REPOSITORY, repo);
- }
-
- params.put(CollectionAdminParams.INDEX_BACKUP_STRATEGY, strategy);
- params.put(CoreAdminParams.BACKUP_INCREMENTAL, incremental);
- return params;
+ final var response =
+ CreateCollectionBackupAPI.invokeFromV1Params(req, rsp,
h.coreContainer);
+ V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, response);
+ return null;
}),
RESTORE_OP(
RESTORE,
@@ -2110,6 +2034,7 @@ public class CollectionsHandler extends
RequestHandlerBase implements Permission
public Collection<Class<? extends JerseyResource>> getJerseyResources() {
return List.of(
AddReplicaPropertyAPI.class,
+ CreateCollectionBackupAPI.class,
DeleteAliasAPI.class,
DeleteCollectionAPI.class,
DeleteReplicaPropertyAPI.class,
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/api/AdminAPIBase.java
b/solr/core/src/java/org/apache/solr/handler/admin/api/AdminAPIBase.java
index 16d4b189d02..4ffe8b83bbc 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/AdminAPIBase.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/AdminAPIBase.java
@@ -19,6 +19,7 @@ package org.apache.solr.handler.admin.api;
import org.apache.solr.api.JerseyResource;
import org.apache.solr.common.SolrException;
+import org.apache.solr.common.cloud.ClusterState;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.logging.MDCLoggingContext;
import org.apache.solr.request.SolrQueryRequest;
@@ -46,6 +47,28 @@ public abstract class AdminAPIBase extends JerseyResource {
return coreContainer;
}
+ protected String resolveAndValidateAliasIfEnabled(
+ String unresolvedCollectionName, boolean aliasResolutionEnabled) {
+ final String resolvedCollectionName =
+ aliasResolutionEnabled ? resolveAlias(unresolvedCollectionName) :
unresolvedCollectionName;
+ final ClusterState clusterState =
coreContainer.getZkController().getClusterState();
+ if (!clusterState.hasCollection(resolvedCollectionName)) {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST,
+ "Collection '" + resolvedCollectionName + "' does not exist, no
action taken.");
+ }
+
+ return resolvedCollectionName;
+ }
+
+ private String resolveAlias(String aliasName) {
+ return coreContainer
+ .getZkController()
+ .getZkStateReader()
+ .getAliases()
+ .resolveSimpleAlias(aliasName);
+ }
+
public static void validateZooKeeperAwareCoreContainer(CoreContainer
coreContainer) {
if (coreContainer == null) {
throw new SolrException(
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollectionBackupAPI.java
b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollectionBackupAPI.java
new file mode 100644
index 00000000000..cfb835ff3ff
--- /dev/null
+++
b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollectionBackupAPI.java
@@ -0,0 +1,267 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.handler.admin.api;
+
+import static
org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
+import static
org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
+import static
org.apache.solr.common.params.CollectionAdminParams.INDEX_BACKUP_STRATEGY;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CommonParams.NAME;
+import static org.apache.solr.common.params.CoreAdminParams.BACKUP_INCREMENTAL;
+import static org.apache.solr.common.params.CoreAdminParams.BACKUP_LOCATION;
+import static org.apache.solr.common.params.CoreAdminParams.BACKUP_REPOSITORY;
+import static org.apache.solr.common.params.CoreAdminParams.COMMIT_NAME;
+import static
org.apache.solr.common.params.CoreAdminParams.MAX_NUM_BACKUP_POINTS;
+import static
org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
+import static
org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.cloud.ClusterProperties;
+import org.apache.solr.common.cloud.ZkNodeProps;
+import org.apache.solr.common.params.CollectionAdminParams;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.common.params.CoreAdminParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.backup.repository.BackupRepository;
+import org.apache.solr.handler.admin.CollectionsHandler;
+import org.apache.solr.jersey.JacksonReflectMapWriter;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SolrJacksonMapper;
+import org.apache.solr.jersey.SolrJerseyResponse;
+import org.apache.solr.jersey.SubResponseAccumulatingJerseyResponse;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.zookeeper.common.StringUtils;
+
+/**
+ * V2 API for creating a new "backup" of a specified collection
+ *
+ * <p>This API is analogous to the v1 /admin/collections?action=BACKUP command.
+ */
+@Path("/collections/{collectionName}/backups/{backupName}/versions")
+public class CreateCollectionBackupAPI extends AdminAPIBase {
+ private final ObjectMapper objectMapper;
+
+ @Inject
+ public CreateCollectionBackupAPI(
+ CoreContainer coreContainer,
+ SolrQueryRequest solrQueryRequest,
+ SolrQueryResponse solrQueryResponse) {
+ super(coreContainer, solrQueryRequest, solrQueryResponse);
+
+ this.objectMapper = SolrJacksonMapper.getObjectMapper();
+ }
+
+ @POST
+ @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+ @PermissionName(COLL_EDIT_PERM)
+ public SolrJerseyResponse createCollectionBackup(
+ @PathParam("collectionName") String collectionName,
+ @PathParam("backupName") String backupName,
+ CreateCollectionBackupRequestBody requestBody)
+ throws Exception {
+ if (requestBody == null) {
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing
required request body");
+ }
+ if (StringUtils.isBlank(backupName)) {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST, "Missing required parameter:
'backupName'");
+ }
+ if (collectionName == null) {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST, "Missing required parameter:
'collection'");
+ }
+ final CoreContainer coreContainer =
fetchAndValidateZooKeeperAwareCoreContainer();
+ recordCollectionForLogAndTracing(collectionName, solrQueryRequest);
+
+ collectionName =
+ resolveAndValidateAliasIfEnabled(
+ collectionName, Boolean.TRUE.equals(requestBody.followAliases));
+
+ final BackupRepository repository =
coreContainer.newBackupRepository(requestBody.repository);
+ requestBody.location = getLocation(repository, requestBody.location);
+ if (requestBody.incremental == null) {
+ requestBody.incremental = Boolean.TRUE;
+ }
+
+ // Check if the specified location is valid for this repository.
+ final URI uri = repository.createDirectoryURI(requestBody.location);
+ try {
+ if (!repository.exists(uri)) {
+ throw new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR, "specified location " + uri
+ " does not exist.");
+ }
+ } catch (IOException ex) {
+ throw new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR,
+ "Failed to check the existence of " + uri + ". Is it valid?",
+ ex);
+ }
+
+ if (requestBody.backupStrategy == null) {
+ requestBody.backupStrategy = CollectionAdminParams.COPY_FILES_STRATEGY;
+ }
+ if
(!CollectionAdminParams.INDEX_BACKUP_STRATEGIES.contains(requestBody.backupStrategy))
{
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST,
+ "Unknown index backup strategy " + requestBody.backupStrategy);
+ }
+
+ final ZkNodeProps remoteMessage = createRemoteMessage(collectionName,
backupName, requestBody);
+ final SolrResponse remoteResponse =
+ CollectionsHandler.submitCollectionApiCommand(
+ coreContainer,
+ coreContainer.getDistributedCollectionCommandRunner(),
+ remoteMessage,
+ CollectionParams.CollectionAction.BACKUP,
+ DEFAULT_COLLECTION_OP_TIMEOUT);
+ if (remoteResponse.getException() != null) {
+ throw remoteResponse.getException();
+ }
+
+ final SolrJerseyResponse response =
+ objectMapper.convertValue(
+ remoteResponse.getResponse(),
CreateCollectionBackupResponseBody.class);
+
+ return response;
+ }
+
+ public static ZkNodeProps createRemoteMessage(
+ String collectionName, String backupName,
CreateCollectionBackupRequestBody requestBody) {
+ final Map<String, Object> remoteMessage = requestBody.toMap(new
HashMap<>());
+ remoteMessage.put(QUEUE_OPERATION,
CollectionParams.CollectionAction.BACKUP.toLower());
+ remoteMessage.put(COLLECTION_PROP, collectionName);
+ remoteMessage.put(NAME, backupName);
+ if (!StringUtils.isBlank(requestBody.backupStrategy)) {
+ remoteMessage.put(INDEX_BACKUP_STRATEGY,
remoteMessage.remove("backupStrategy"));
+ }
+ if (!StringUtils.isBlank(requestBody.snapshotName)) {
+ remoteMessage.put(COMMIT_NAME, remoteMessage.remove("snapshotName"));
+ }
+ return new ZkNodeProps(remoteMessage);
+ }
+
+ public static CreateCollectionBackupRequestBody
createRequestBodyFromV1Params(SolrParams params) {
+ var requestBody = new
CreateCollectionBackupAPI.CreateCollectionBackupRequestBody();
+
+ requestBody.location = params.get(BACKUP_LOCATION);
+ requestBody.repository = params.get(BACKUP_REPOSITORY);
+ requestBody.followAliases = params.getBool(FOLLOW_ALIASES);
+ requestBody.backupStrategy = params.get(INDEX_BACKUP_STRATEGY);
+ requestBody.snapshotName = params.get(COMMIT_NAME);
+ requestBody.incremental = params.getBool(BACKUP_INCREMENTAL);
+ requestBody.maxNumBackupPoints = params.getInt(MAX_NUM_BACKUP_POINTS);
+ requestBody.async = params.get(ASYNC);
+
+ return requestBody;
+ }
+
+ public static SolrJerseyResponse invokeFromV1Params(
+ SolrQueryRequest req, SolrQueryResponse rsp, CoreContainer
coreContainer) throws Exception {
+ req.getParams().required().check(NAME, COLLECTION_PROP);
+ final var collectionName = req.getParams().get(COLLECTION_PROP);
+ final var backupName = req.getParams().get(NAME);
+ final var requestBody = createRequestBodyFromV1Params(req.getParams());
+
+ final var createBackupApi = new CreateCollectionBackupAPI(coreContainer,
req, rsp);
+ return createBackupApi.createCollectionBackup(collectionName, backupName,
requestBody);
+ }
+
+ private String getLocation(BackupRepository repository, String location)
throws IOException {
+ location = repository.getBackupLocation(location);
+ if (location != null) {
+ return location;
+ }
+
+ // Refresh the cluster property file to make sure the value set for
location is the
+ // latest. Check if the location is specified in the cluster property.
+ location =
+ new ClusterProperties(coreContainer.getZkController().getZkClient())
+ .getClusterProperty(CoreAdminParams.BACKUP_LOCATION, null);
+ if (location != null) {
+ return location;
+ }
+
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST,
+ "'location' is not specified as a query"
+ + " parameter or as a default repository property or as a cluster
property.");
+ }
+
+ public static class CreateCollectionBackupRequestBody implements
JacksonReflectMapWriter {
+ @JsonProperty public String location;
+ @JsonProperty public String repository;
+ @JsonProperty public Boolean followAliases;
+ @JsonProperty public String backupStrategy;
+ @JsonProperty public String snapshotName;
+ @JsonProperty public Boolean incremental;
+ @JsonProperty public Integer maxNumBackupPoints;
+ @JsonProperty public String async;
+ }
+
+ public static class CreateCollectionBackupResponseBody
+ extends SubResponseAccumulatingJerseyResponse {
+ @JsonProperty("response")
+ public CollectionBackupData backupDataResponse;
+
+ @JsonProperty("deleted")
+ public List<BackupDeletionData> deleted;
+
+ @JsonProperty public String collection;
+ }
+
+ public static class CollectionBackupData implements JacksonReflectMapWriter {
+ @JsonProperty public String collection;
+ @JsonProperty public Integer numShards;
+ @JsonProperty public Integer backupId;
+ @JsonProperty public String indexVersion;
+ @JsonProperty public String startTime;
+ @JsonProperty public String endTime;
+ @JsonProperty public Integer indexFileCount;
+ @JsonProperty public Integer uploadedIndexFileCount;
+ @JsonProperty public Double indexSizeMB;
+
+ @JsonProperty("uploadedIndexFileMB")
+ public Double uploadedIndexSizeMB;
+
+ @JsonProperty public List<String> shardBackupIds;
+ }
+
+ public static class BackupDeletionData implements JacksonReflectMapWriter {
+ @JsonProperty public String startTime;
+ @JsonProperty public Integer backupId;
+ @JsonProperty public Long size;
+ @JsonProperty public Integer numFiles;
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/jersey/SolrJacksonMapper.java
b/solr/core/src/java/org/apache/solr/jersey/SolrJacksonMapper.java
index 00513d6ce82..77b4ca51105 100644
--- a/solr/core/src/java/org/apache/solr/jersey/SolrJacksonMapper.java
+++ b/solr/core/src/java/org/apache/solr/jersey/SolrJacksonMapper.java
@@ -32,8 +32,19 @@ import org.apache.solr.common.util.NamedList;
@SuppressWarnings("rawtypes")
@Provider
public class SolrJacksonMapper implements ContextResolver<ObjectMapper> {
+
+ private static final ObjectMapper objectMapper = createObjectMapper();
+
@Override
public ObjectMapper getContext(Class<?> type) {
+ return objectMapper;
+ }
+
+ public static ObjectMapper getObjectMapper() {
+ return objectMapper;
+ }
+
+ private static ObjectMapper createObjectMapper() {
final SimpleModule customTypeModule = new SimpleModule();
customTypeModule.addSerializer(new NamedListSerializer(NamedList.class));
diff --git
a/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
b/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
index 6c904312160..ab89ec0237e 100644
--- a/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
@@ -192,19 +192,15 @@ public class V2ApiIntegrationTest extends
SolrCloudTestCase {
"/collections/collection1/get",
Utils.getObjectByPath(result, true, "/spec[0]/url/paths[0]"));
String tempDir = createTempDir().toFile().getPath();
- Map<String, Object> backupPayload = new HashMap<>();
Map<String, Object> backupParams = new HashMap<>();
- backupPayload.put("backup-collection", backupParams);
- backupParams.put("name", "backup_test");
- backupParams.put("collection", COLL_NAME);
backupParams.put("location", tempDir);
cluster
.getJettySolrRunners()
.forEach(j ->
j.getCoreContainer().getAllowPaths().add(Paths.get(tempDir)));
client.request(
- new V2Request.Builder("/c")
+ new V2Request.Builder("/collections/" + COLL_NAME +
"/backups/backup_test/versions")
.withMethod(SolrRequest.METHOD.POST)
- .withPayload(Utils.toJSONString(backupPayload))
+ .withPayload(Utils.toJSONString(backupParams))
.build());
}
diff --git
a/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionsAPIMappingTest.java
b/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionsAPIMappingTest.java
index d37d9146c21..439ec2b567f 100644
---
a/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionsAPIMappingTest.java
+++
b/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionsAPIMappingTest.java
@@ -175,36 +175,6 @@ public class V2CollectionsAPIMappingTest extends
V2ApiMappingTest<CollectionsHan
RoutedAlias.CREATE_COLLECTION_PREFIX +
ZkStateReader.REPLICATION_FACTOR));
}
- @Test
- public void testBackupAllProperties() throws Exception {
- final SolrParams v1Params =
- captureConvertedV1Params(
- "/collections",
- "POST",
- "{'backup-collection': {"
- + "'name': 'backupName', "
- + "'collection': 'collectionName', "
- + "'location': '/some/location/uri', "
- + "'repository': 'someRepository', "
- + "'followAliases': true, "
- + "'indexBackup': 'copy-files', "
- + "'commitName': 'someSnapshotName', "
- + "'incremental': true, "
- + "'async': 'requestTrackingId' "
- + "}}");
-
- assertEquals(CollectionParams.CollectionAction.BACKUP.lowerName,
v1Params.get(ACTION));
- assertEquals("backupName", v1Params.get(CommonParams.NAME));
- assertEquals("collectionName",
v1Params.get(BackupManager.COLLECTION_NAME_PROP));
- assertEquals("/some/location/uri",
v1Params.get(CoreAdminParams.BACKUP_LOCATION));
- assertEquals("someRepository",
v1Params.get(CoreAdminParams.BACKUP_REPOSITORY));
-
assertTrue(v1Params.getPrimitiveBool(CollectionAdminParams.FOLLOW_ALIASES));
- assertEquals("copy-files",
v1Params.get(CollectionAdminParams.INDEX_BACKUP_STRATEGY));
- assertEquals("someSnapshotName",
v1Params.get(CoreAdminParams.COMMIT_NAME));
- assertTrue(v1Params.getPrimitiveBool(CoreAdminParams.BACKUP_INCREMENTAL));
- assertEquals("requestTrackingId", v1Params.get(CommonAdminParams.ASYNC));
- }
-
@Test
public void testRestoreAllProperties() throws Exception {
final SolrParams v1Params =
diff --git
a/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionBackupApiTest.java
b/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionBackupApiTest.java
new file mode 100644
index 00000000000..f8c51b1a8d0
--- /dev/null
+++
b/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionBackupApiTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.handler.admin.api;
+
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static
org.apache.solr.common.params.CollectionAdminParams.COPY_FILES_STRATEGY;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.junit.Test;
+
+/** Unit tests for {@link CreateCollectionBackupAPI} */
+public class V2CollectionBackupApiTest extends SolrTestCaseJ4 {
+ @Test
+ public void testCreateRemoteMessageWithAllProperties() {
+ final var requestBody = new
CreateCollectionBackupAPI.CreateCollectionBackupRequestBody();
+ requestBody.location = "/some/location";
+ requestBody.repository = "someRepoName";
+ requestBody.followAliases = true;
+ requestBody.backupStrategy = COPY_FILES_STRATEGY;
+ requestBody.snapshotName = "someSnapshotName";
+ requestBody.incremental = true;
+ requestBody.maxNumBackupPoints = 123;
+ requestBody.async = "someId";
+
+ var message =
+ CreateCollectionBackupAPI.createRemoteMessage(
+ "someCollectionName", "someBackupName", requestBody);
+ var messageProps = message.getProperties();
+
+ assertEquals(11, messageProps.size());
+ assertEquals("someCollectionName", messageProps.get("collection"));
+ assertEquals("/some/location", messageProps.get("location"));
+ assertEquals("someRepoName", messageProps.get("repository"));
+ assertEquals(true, messageProps.get("followAliases"));
+ assertEquals("copy-files", messageProps.get("indexBackup"));
+ assertEquals("someSnapshotName", messageProps.get("commitName"));
+ assertEquals(true, messageProps.get("incremental"));
+ assertEquals(123, messageProps.get("maxNumBackupPoints"));
+ assertEquals("someId", messageProps.get("async"));
+ assertEquals("backup", messageProps.get(QUEUE_OPERATION));
+ assertEquals("someBackupName", messageProps.get("name"));
+ }
+
+ @Test
+ public void testCreateRemoteMessageOmitsNullValues() {
+ final var requestBody = new
CreateCollectionBackupAPI.CreateCollectionBackupRequestBody();
+ requestBody.location = "/some/location";
+
+ var message =
+ CreateCollectionBackupAPI.createRemoteMessage(
+ "someCollectionName", "someBackupName", requestBody);
+ var messageProps = message.getProperties();
+
+ assertEquals(4, messageProps.size());
+ assertEquals("someCollectionName", messageProps.get("collection"));
+ assertEquals("/some/location", messageProps.get("location"));
+ assertEquals("backup", messageProps.get(QUEUE_OPERATION));
+ assertEquals("someBackupName", messageProps.get("name"));
+ }
+
+ @Test
+ public void testCanCreateV2RequestBodyFromV1Params() {
+ final var params = new ModifiableSolrParams();
+ params.set("collection", "someCollectionName");
+ params.set("location", "/some/location");
+ params.set("repository", "someRepoName");
+ params.set("followAliases", "true");
+ params.set("indexBackup", COPY_FILES_STRATEGY);
+ params.set("commitName", "someSnapshotName");
+ params.set("incremental", "true");
+ params.set("maxNumBackupPoints", "123");
+ params.set("async", "someId");
+
+ final var requestBody =
CreateCollectionBackupAPI.createRequestBodyFromV1Params(params);
+
+ assertEquals("/some/location", requestBody.location);
+ assertEquals("someRepoName", requestBody.repository);
+ assertEquals(Boolean.TRUE, requestBody.followAliases);
+ assertEquals("copy-files", requestBody.backupStrategy);
+ assertEquals("someSnapshotName", requestBody.snapshotName);
+ assertEquals(Boolean.TRUE, requestBody.incremental);
+ assertEquals(Integer.valueOf(123), requestBody.maxNumBackupPoints);
+ assertEquals("someId", requestBody.async);
+ }
+}
diff --git
a/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
b/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
index aeb9c249b9d..3cfb11536aa 100644
---
a/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
+++
b/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
@@ -1542,7 +1542,7 @@
http://localhost:8983/solr/admin/collections?action=COLSTATUS&collection=getting
[[backup]]
== BACKUP: Backup Collection
-Backs up Solr collections and associated configurations to a shared filesystem
- for example a Network File System.
+Backs up Solr collections and associated configurations to a "backup
repository".
[.dynamic-tabs]
--
@@ -1561,24 +1561,18 @@
http://localhost:8983/solr/admin/collections?action=BACKUP&name=techproducts_bac
====
[.tab-label]*V2 API*
-With the v2 API, the `backup-collection` command is provided as part of the
JSON data that contains the required parameters:
-
[source,bash]
----
-curl -X POST http://localhost:8983/api/collections -H 'Content-Type:
application/json' -d '
+curl -X POST
http://localhost:8983/api/collections/techproducts/backups/techproducts_backup/versions
-H 'Content-Type: application/json' -d '
{
- "backup-collection": {
- "name": "techproducts_backup",
- "collection": "techproducts",
- "location": "file:///path/to/my/shared/drive"
- }
+ "location": "file:///path/to/my/shared/drive"
}
'
----
====
--
-The BACKUP command will backup Solr indexes and configurations for a specified
collection.
+The BACKUP API will backup Solr indexes and configurations for a specified
collection.
The BACKUP command xref:backup-restore.adoc[takes one copy from each shard for
the indexes].
For configurations, it backs up the configset that was associated with the
collection and metadata.
@@ -1607,6 +1601,7 @@ s|Required |Default: none
|===
+
The name of the collection to be backed up.
+Provided as a query parameter for v1 requests, and as a path segment for v2
requests.
`name`::
+
@@ -1616,6 +1611,7 @@ s|Required |Default: none
|===
+
What to name the backup that is created.
+Provided as a query parameter for v1 requests, or as a path segment for v2
requests.
This is checked to make sure it doesn't already exist, and otherwise an error
message is raised.
`location`::
@@ -1677,6 +1673,25 @@ A boolean parameter allowing users to choose whether to
create an incremental (`
If unspecified, backups are done incrementally by default.
Incremental backups are preferred in all known circumstances and snapshot
backups are deprecated, so this parameter should only be used after much
consideration.
+`indexBackup` (v1), `backupStrategy` (v2)::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: "copy-files"
+|===
++
+A string parameter allowing users to specify one of several different backup
"strategies".
+Valid options are `copy-files` (which backs up both the collection configset
and index data), and `none` (which will only backup the collection configset).
+
+`commitName` (v1), `snapshotName` (v2)::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: none
+|===
++
+The name of a the collection "snapshot" to create a backup from.
+If not provided, Solr will create the backup from the current collection state
(instead of a previous snapshotted state).
[example.tab-pane#backup-response-incremental]
====
diff --git
a/solr/test-framework/src/java/org/apache/solr/cloud/api/collections/AbstractIncrementalBackupTest.java
b/solr/test-framework/src/java/org/apache/solr/cloud/api/collections/AbstractIncrementalBackupTest.java
index 08070802011..d43fff4f38f 100644
---
a/solr/test-framework/src/java/org/apache/solr/cloud/api/collections/AbstractIncrementalBackupTest.java
+++
b/solr/test-framework/src/java/org/apache/solr/cloud/api/collections/AbstractIncrementalBackupTest.java
@@ -495,7 +495,7 @@ public abstract class AbstractIncrementalBackupTest extends
SolrCloudTestCase {
this.maxNumberOfBackupToKeep = maxNumberOfBackupToKeep;
}
- @SuppressWarnings("rawtypes")
+ @SuppressWarnings({"rawtypes", "unchecked"})
private void backupThenWait() throws SolrServerException, IOException {
CollectionAdminRequest.Backup backup =
CollectionAdminRequest.backupCollection(getCollectionName(),
backupName)
@@ -515,7 +515,7 @@ public abstract class AbstractIncrementalBackupTest extends
SolrCloudTestCase {
} else {
CollectionAdminResponse rsp = backup.process(cluster.getSolrClient());
assertEquals(0, rsp.getStatus());
- NamedList resp = (NamedList) rsp.getResponse().get("response");
+ Map<String, Object> resp = (Map<String, Object>)
rsp.getResponse().get("response");
numBackup++;
assertEquals(numBackup, resp.get("backupId"));
;