diff --git a/changelog/unreleased/SOLR-18320-skip-zk-for-configset-upload.yml b/changelog/unreleased/SOLR-18320-skip-zk-for-configset-upload.yml new file mode 100644 index 000000000000..fa9d69fa2f78 --- /dev/null +++ b/changelog/unreleased/SOLR-18320-skip-zk-for-configset-upload.yml @@ -0,0 +1,7 @@ +title: bin/solr create no longer requires a direct ZooKeeper connection to upload a configset; it now uses the Configsets V2 API. +type: changed +authors: + - name: Eric Pugh +links: + - name: SOLR-18320 + url: https://issues.apache.org/jira/browse/SOLR-18320 diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java b/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java index dae7174605ad..20cf3b5820e5 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java @@ -31,7 +31,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** Supports zk upconfig command in the bin/solr script. */ +/** + * Supports zk upconfig command in the bin/solr script. + * + *
Unlike {@link CreateTool}, this uploads directly to ZooKeeper rather than going through the
+ * Configsets V2 API. That's intentional: this command is meant to work against a bare ZooKeeper
+ * ensemble with no Solr node running yet (e.g. provisioning configs before the cluster starts), and
+ * the V2 API requires an HTTP endpoint on an already-running node to PUT the zip to.
+ */
@SuppressWarnings("UnnecessarilyFullyQualified")
@picocli.CommandLine.Command(
name = "upconfig",
diff --git a/solr/core/src/java/org/apache/solr/cli/CreateTool.java b/solr/core/src/java/org/apache/solr/cli/CreateTool.java
index 58149b974757..9e58a157ab2e 100644
--- a/solr/core/src/java/org/apache/solr/cli/CreateTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/CreateTool.java
@@ -16,6 +16,7 @@
*/
package org.apache.solr.cli;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -34,12 +35,13 @@
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
import org.apache.solr.client.solrj.request.CollectionsApi;
+import org.apache.solr.client.solrj.request.ConfigsetsApi;
import org.apache.solr.client.solrj.request.CoresApi;
import org.apache.solr.client.solrj.request.SystemInfoRequest;
import org.apache.solr.client.solrj.response.SystemInfoResponse;
-import org.apache.solr.cloud.ZkConfigSetService;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.util.EnvUtils;
+import org.apache.solr.common.util.Utils;
import org.apache.solr.core.ConfigSetService;
/** Supports create command in the bin/solr script. */
@@ -317,24 +319,22 @@ private void createCollection(CloudSolrClient cloudSolrClient, CreateParams para
int replicationFactor = params.replicationFactor;
String confName = params.confName;
- boolean configExistsInZk =
+ boolean configExists =
confName != null
&& !confName.trim().isEmpty()
- && ZkStateReader.from(cloudSolrClient).getZkClient().exists("/configs/" + confName);
+ && new ConfigsetsApi.ListConfigSet()
+ .process(cloudSolrClient)
+ .configSets
+ .contains(confName);
- if (configExistsInZk) {
+ if (configExists) {
echo("Re-using existing configuration directory " + confName);
- } else { // if (confdir != null && !confdir.trim().isEmpty()) {
+ } else {
if (confName == null || confName.trim().isEmpty()) {
confName = params.name;
}
- // TODO: This should be done using the configSet API. This would let us remove
- // the direct dependency on ZooKeeper APIs. Unlike the bin/solr zk comamnds that
- // work directly with ZooKeeper.
final Path configsetsDirPath = CLIUtils.getConfigSetsDir(solrInstallDirPath);
- ConfigSetService configSetService =
- new ZkConfigSetService(ZkStateReader.from(cloudSolrClient).getZkClient());
Path confPath =
ConfigSetService.getConfigsetPath(params.confDir, configsetsDirPath.toString());
@@ -343,10 +343,11 @@ private void createCollection(CloudSolrClient cloudSolrClient, CreateParams para
+ confPath.toAbsolutePath()
+ " for config "
+ confName
- + " to ZooKeeper at "
- + cloudSolrClient.getClusterStateProvider().getQuorumHosts());
- // We will trust the config since we have the Zookeeper Address
- configSetService.uploadConfig(confName, confPath);
+ + " using the Configsets V2 API");
+ var uploadReq =
+ new ConfigsetsApi.UploadConfigSet(
+ confName, new ByteArrayInputStream(zipConfigSet(confPath)));
+ uploadReq.process(cloudSolrClient);
}
// since creating a collection is a heavy-weight operation, check for existence first
@@ -387,6 +388,11 @@ private void createCollection(CloudSolrClient cloudSolrClient, CreateParams para
echo(endMessage);
}
+ /** Zips the contents of a configset directory for upload. */
+ static byte[] zipConfigSet(Path confPath) throws IOException {
+ return Utils.zipDirectory(confPath);
+ }
+
private Path getFullConfDir(Path solrInstallDir, Path confDirName) {
return CLIUtils.getConfigSetsDir(solrInstallDir).resolve(confDirName);
}
diff --git a/solr/core/src/java/org/apache/solr/handler/configsets/DownloadConfigSet.java b/solr/core/src/java/org/apache/solr/handler/configsets/DownloadConfigSet.java
index 729aaf00d914..76d12758db01 100644
--- a/solr/core/src/java/org/apache/solr/handler/configsets/DownloadConfigSet.java
+++ b/solr/core/src/java/org/apache/solr/handler/configsets/DownloadConfigSet.java
@@ -21,20 +21,14 @@
import jakarta.inject.Inject;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.StreamingOutput;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
import org.apache.commons.io.file.PathUtils;
import org.apache.solr.client.api.endpoint.ConfigsetsApi;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.StrUtils;
+import org.apache.solr.common.util.Utils;
import org.apache.solr.core.ConfigSetService;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.jersey.PermissionName;
@@ -86,48 +80,12 @@ public static Response buildZipResponse(ConfigSetService configSetService, Strin
*/
public static byte[] zipConfigSet(ConfigSetService configSetService, String configSetName)
throws IOException {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
Path tmpDirectory = Files.createTempDirectory("configset-download-");
try {
configSetService.downloadConfig(configSetName, tmpDirectory);
- try (ZipOutputStream zipOut = new ZipOutputStream(baos)) {
- Files.walkFileTree(
- tmpDirectory,
- new SimpleFileVisitor<>() {
- @Override
- public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
- throws IOException {
- if (Files.isHidden(dir)) {
- return FileVisitResult.SKIP_SUBTREE;
- }
- String dirName = tmpDirectory.relativize(dir).toString();
- if (!dirName.isEmpty()) {
- if (!dirName.endsWith("/")) {
- dirName += "/";
- }
- zipOut.putNextEntry(new ZipEntry(dirName));
- zipOut.closeEntry();
- }
- return FileVisitResult.CONTINUE;
- }
-
- @Override
- public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
- throws IOException {
- if (!Files.isHidden(file)) {
- try (InputStream fis = Files.newInputStream(file)) {
- ZipEntry zipEntry = new ZipEntry(tmpDirectory.relativize(file).toString());
- zipOut.putNextEntry(zipEntry);
- fis.transferTo(zipOut);
- }
- }
- return FileVisitResult.CONTINUE;
- }
- });
- }
+ return Utils.zipDirectory(tmpDirectory);
} finally {
PathUtils.deleteDirectory(tmpDirectory);
}
- return baos.toByteArray();
}
}
diff --git a/solr/core/src/test/org/apache/solr/cli/CreateToolTest.java b/solr/core/src/test/org/apache/solr/cli/CreateToolTest.java
index e1df239b51bb..a814ba529dec 100644
--- a/solr/core/src/test/org/apache/solr/cli/CreateToolTest.java
+++ b/solr/core/src/test/org/apache/solr/cli/CreateToolTest.java
@@ -17,6 +17,13 @@
package org.apache.solr.cli;
+import java.io.ByteArrayInputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.apache.solr.util.SecurityJson;
import org.junit.BeforeClass;
@@ -57,4 +64,52 @@ public void testCreateCollectionWithBasicAuth() throws Exception {
assertEquals(0, runTool(args, CreateTool.class));
}
+
+ @Test
+ public void testCreateCollectionUploadsNewConfigSet() throws Exception {
+ String[] args = {
+ "create",
+ "-c",
+ "testCreateCollectionUploadsNewConfigSet",
+ "-d",
+ configset("cloud-minimal").toString(),
+ "-n",
+ "cloud-minimal-uploaded",
+ "-z",
+ cluster.getZkClient().getZkServerAddress(),
+ "--credentials",
+ SecurityJson.USER_PASS,
+ "--verbose"
+ };
+
+ assertEquals(0, CLITestHelper.runTool(args, CreateTool.class));
+ assertTrue(cluster.getZkClient().exists("/configs/cloud-minimal-uploaded"));
+ }
+
+ @Test
+ public void testZipConfigSetSkipsHiddenFilesAndIncludesDirectoryEntries() throws Exception {
+ Path confDir = createTempDir("zipConfigSetTest");
+ Files.writeString(confDir.resolve("solrconfig.xml"), "