Skip to content
73 changes: 52 additions & 21 deletions solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,21 @@
*/
package org.apache.solr.cli;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.request.GenericV2SolrRequest;
import org.apache.solr.common.cloud.ZkMaintenanceUtils;
import org.apache.solr.core.ConfigSetService;
import org.apache.solr.util.FileTypeMagicUtil;
Expand All @@ -38,7 +47,7 @@ public class ConfigSetUploadTool extends ToolBase {
.hasArg()
.argName("NAME")
.required()
.desc("Configset name in ZooKeeper.")
.desc("Configset name.")
.get();

private static final Option CONF_DIR_OPTION =
Expand Down Expand Up @@ -75,36 +84,58 @@ public String getUsage() {

@Override
public void runImpl(CommandLine cli) throws Exception {
String zkHost = CLIUtils.getZkHost(cli);

final String solrInstallDir = System.getProperty("solr.install.dir");
Path solrInstallDirPath = Path.of(solrInstallDir);

String confName = cli.getOptionValue(CONF_NAME_OPTION);
String confDir = cli.getOptionValue(CONF_DIR_OPTION);

echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ...");
try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) {
final Path configsetsDirPath = CLIUtils.getConfigSetsDir(solrInstallDirPath);
Path confPath = ConfigSetService.getConfigsetPath(confDir, configsetsDirPath.toString());
final Path configsetsDirPath = CLIUtils.getConfigSetsDir(solrInstallDirPath);
Path confPath = ConfigSetService.getConfigsetPath(confDir, configsetsDirPath.toString());

echo("Uploading " + confPath.toAbsolutePath() + " for config " + confName + " to Solr");

echo(
"Uploading "
+ confPath.toAbsolutePath()
+ " for config "
+ confName
+ " to ZooKeeper at "
+ zkHost);
FileTypeMagicUtil.assertConfigSetFolderLegal(confPath);
ZkMaintenanceUtils.uploadToZK(
zkClient,
confPath,
ZkMaintenanceUtils.CONFIGS_ZKNODE + "/" + confName,
ZkMaintenanceUtils.UPLOAD_FILENAME_EXCLUDE_PATTERN);
FileTypeMagicUtil.assertConfigSetFolderLegal(confPath);

try (var solrClient = CLIUtils.getSolrClient(cli)) {
byte[] zipData = createZipData(confPath);
var request = new GenericV2SolrRequest(SolrRequest.METHOD.PUT, "/configsets/" + confName);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We have a SolrJ end point to use... Need to dig into our annotations?

request.withContent(zipData, "application/octet-stream");
request.process(solrClient);
} catch (Exception e) {
log.error("Could not complete upconfig operation for reason: ", e);
throw (e);
}
}

private static byte[] createZipData(Path confPath) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
URI base = confPath.toUri();
Deque<Path> queue = new ArrayDeque<>();
queue.push(confPath);
try (ZipOutputStream zout = new ZipOutputStream(baos)) {
while (!queue.isEmpty()) {
Path dir = queue.pop();
try (var files = Files.list(dir)) {
for (Path file : files.toList()) {
String filename = file.getFileName().toString();
if (ZkMaintenanceUtils.UPLOAD_FILENAME_EXCLUDE_PATTERN.matcher(filename).matches()) {
continue;
}
String name = base.relativize(file.toUri()).getPath();
if (Files.isDirectory(file)) {
queue.push(file);
} else {
zout.putNextEntry(new ZipEntry(name));
try (var in = Files.newInputStream(file)) {
in.transferTo(zout);
}
zout.closeEntry();
}
}
}
}
}
return baos.toByteArray();
}
}
210 changes: 210 additions & 0 deletions solr/core/src/test/org/apache/solr/cli/ConfigSetDownloadToolTest.java
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I wanted to balance the ConfigSetUploadToolTest.java testing pattern, so I pulled this out of ZkSubCommandsTest.java.

Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*
* 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.cli;

import java.io.IOException;
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.concurrent.TimeUnit;
import org.apache.solr.cloud.AbstractFullDistribZkTestBase;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.cloud.ZkMaintenanceUtils;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.data.Stat;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

/** Tests for {@link ConfigSetDownloadTool}. */
public class ConfigSetDownloadToolTest extends SolrCloudTestCase {

private static String zkAddr;
private static SolrZkClient zkClient;

@BeforeClass
public static void setupCluster() throws Exception {
configureCluster(1)
.addConfig(
"conf1", TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf"))
.configure();
zkAddr = cluster.getZkServer().getZkAddress();
zkClient =
new SolrZkClient.Builder()
.withUrl(zkAddr)
.withTimeout(30000, TimeUnit.MILLISECONDS)
.build();
System.setProperty("solr.solr.home", TEST_HOME().toString());
}

@AfterClass
public static void closeConn() {
if (null != zkClient) {
zkClient.close();
zkClient = null;
}
zkAddr = null;
}

@Test
public void testDownconfig() throws Exception {
String solrUrl = cluster.getJettySolrRunner(0).getBaseUrl().toString();
Path configSet = TEST_PATH().resolve("configsets");
Path srcConfDir = configSet.resolve("cloud-subdirs").resolve("conf");

// First upload via the HTTP API tool
String[] args =
new String[] {
"upconfig",
"--conf-name",
"downconfig1",
"--conf-dir",
configSet.resolve("cloud-subdirs").toString(),
"-s",
solrUrl
};
assertEquals(
"upconfig should succeed", 0, CLITestHelper.runTool(args, ConfigSetUploadTool.class));

// Now download via the ZK-based tool
Path tmp = createTempDir("downConfigToolTest");
args =
new String[] {
"downconfig", "--conf-name", "downconfig1", "--conf-dir", tmp.toString(), "-z", zkAddr
};

assertEquals(
"downconfig should succeed", 0, CLITestHelper.runTool(args, ConfigSetDownloadTool.class));

// Verify all uploaded files are present locally.
// The download tool writes files to a "conf" subdir, and the ZK root lacks the "conf/" prefix
// because getConfigsetPath() navigates into the conf/ dir automatically during upload.
verifyZkLocalPathsMatch(srcConfDir, "/configs/downconfig1");
}

@Test
public void testDownconfigEmptyFile() throws Exception {
String solrUrl = cluster.getJettySolrRunner(0).getBaseUrl().toString();
Path configSet = TEST_PATH().resolve("configsets");

// Upload the configset
String[] args =
new String[] {
"upconfig",
"--conf-name",
"downconfig2",
"--conf-dir",
configSet.resolve("cloud-subdirs").toString(),
"-s",
solrUrl
};
assertEquals(
"upconfig should succeed", 0, CLITestHelper.runTool(args, ConfigSetUploadTool.class));

// Download it
Path tmp = createTempDir("downConfigEmptyTest").resolve("myconfset");
args =
new String[] {
"downconfig", "--conf-name", "downconfig2", "--conf-dir", tmp.toString(), "-z", zkAddr
};
assertEquals(
"downconfig should succeed", 0, CLITestHelper.runTool(args, ConfigSetDownloadTool.class));

// Create an empty file in the downloaded config
Path emptyFile = tmp.resolve("conf").resolve("stopwords").resolve("emptyfile");
Files.createFile(emptyFile);

// Upload it again (with the empty file included via the ZK-compatible copyConfigUp helper)
AbstractFullDistribZkTestBase.copyConfigUp(
tmp.getParent(), "myconfset", "downconfig2b", zkAddr);

// Download back
Path tmp2 = createTempDir("downConfigEmptyTest2");
args =
new String[] {
"downconfig", "--conf-name", "downconfig2b", "--conf-dir", tmp2.toString(), "-z", zkAddr
};
assertEquals(
"downconfig should succeed", 0, CLITestHelper.runTool(args, ConfigSetDownloadTool.class));

Path destEmpty = tmp2.resolve("conf").resolve("stopwords").resolve("emptyfile");
assertTrue(
"Empty files should NOT be copied down as directories", Files.isRegularFile(destEmpty));
}

private void verifyZkLocalPathsMatch(Path fileRoot, String zkRoot)
throws IOException, KeeperException, InterruptedException {
verifyAllFilesAreZNodes(fileRoot, zkRoot);
verifyAllZNodesAreFiles(fileRoot, zkRoot);
}

private static boolean isEphemeral(String zkPath) throws KeeperException, InterruptedException {
Stat znodeStat = zkClient.exists(zkPath, null);
return znodeStat.getEphemeralOwner() != 0;
}

private void verifyAllZNodesAreFiles(Path fileRoot, String zkRoot)
throws KeeperException, InterruptedException {
for (String child : zkClient.getChildren(zkRoot, null)) {
if (!zkRoot.endsWith("/")) {
zkRoot += "/";
}
if (isEphemeral(zkRoot + child)) continue;

Path thisPath = fileRoot.resolve(child);
assertTrue(
"Znode " + child + " should have been found on disk at " + fileRoot,
Files.exists(thisPath));
verifyAllZNodesAreFiles(thisPath, zkRoot + child);
}
}

private void verifyAllFilesAreZNodes(Path fileRoot, String zkRoot) throws IOException {
Files.walkFileTree(
fileRoot,
new SimpleFileVisitor<Path>() {
void checkPathOnZk(Path path) {
String znode = ZkMaintenanceUtils.createZkNodeName(zkRoot, fileRoot, path);
try {
assertTrue("Should have found " + znode + " on Zookeeper", zkClient.exists(znode));
} catch (Exception e) {
fail(
"Caught unexpected exception "
+ e.getMessage()
+ " Znode we were checking "
+ znode);
}
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
assertTrue("Path should start at proper place!", file.startsWith(fileRoot));
checkPathOnZk(file);
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
checkPathOnZk(dir);
return FileVisitResult.CONTINUE;
}
});
}
}
Loading
Loading