From a1b538122cdee5aba6c494c4941cf5bb55182d17 Mon Sep 17 00:00:00 2001 From: Andy Bristol Date: Wed, 23 May 2018 10:37:57 -0700 Subject: [PATCH] [test] java tests for archive packaging (#30734) Ports the first couple tests for archive distributions from the old bats project to the new java project that includes windows platforms, consolidating them into one test method that tests that the distributions can be extracted and their contents verified. Includes the zip distributions which were not tested in the bats project. --- TESTING.asciidoc | 28 +- Vagrantfile | 1 + .../gradle/vagrant/VagrantTestPlugin.groovy | 23 +- qa/vagrant/build.gradle | 4 +- .../packaging/PackagingTests.java | 23 +- .../elasticsearch/packaging/VMTestRunner.java | 40 +++ .../packaging/test/ArchiveTestCase.java | 65 +++++ .../packaging/test/DefaultTarTests.java | 30 +++ .../packaging/test/DefaultZipTests.java | 30 +++ .../packaging/test/OssTarTests.java | 30 +++ .../packaging/test/OssZipTests.java | 30 +++ .../packaging/util/Archives.java | 239 ++++++++++++++++++ .../elasticsearch/packaging/util/Cleanup.java | 121 +++++++++ .../packaging/util/Distribution.java | 76 ++++++ .../packaging/util/FileMatcher.java | 137 ++++++++++ .../packaging/util/FileUtils.java | 134 ++++++++++ .../packaging/util/Installation.java | 58 +++++ .../packaging/util/Platforms.java | 68 +++++ .../elasticsearch/packaging/util/Shell.java | 193 ++++++++++++++ 19 files changed, 1313 insertions(+), 17 deletions(-) create mode 100644 qa/vagrant/src/main/java/org/elasticsearch/packaging/VMTestRunner.java create mode 100644 qa/vagrant/src/main/java/org/elasticsearch/packaging/test/ArchiveTestCase.java create mode 100644 qa/vagrant/src/main/java/org/elasticsearch/packaging/test/DefaultTarTests.java create mode 100644 qa/vagrant/src/main/java/org/elasticsearch/packaging/test/DefaultZipTests.java create mode 100644 qa/vagrant/src/main/java/org/elasticsearch/packaging/test/OssTarTests.java create mode 100644 qa/vagrant/src/main/java/org/elasticsearch/packaging/test/OssZipTests.java create mode 100644 qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Archives.java create mode 100644 qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Cleanup.java create mode 100644 qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Distribution.java create mode 100644 qa/vagrant/src/main/java/org/elasticsearch/packaging/util/FileMatcher.java create mode 100644 qa/vagrant/src/main/java/org/elasticsearch/packaging/util/FileUtils.java create mode 100644 qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Installation.java create mode 100644 qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Platforms.java create mode 100644 qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Shell.java diff --git a/TESTING.asciidoc b/TESTING.asciidoc index 1e984a17f3c..5e5207b279e 100644 --- a/TESTING.asciidoc +++ b/TESTING.asciidoc @@ -512,7 +512,9 @@ into it vagrant ssh debian-9 -------------------------------------------- -Now inside the VM, to run the https://github.com/sstephenson/bats[bats] packaging tests +Now inside the VM, start the packaging tests from the terminal. There are two packaging +test projects. The old ones are written with https://github.com/sstephenson/bats[bats] +and only run on linux. To run them do -------------------------------------------- cd $PACKAGING_ARCHIVES @@ -524,18 +526,36 @@ sudo bats $BATS_TESTS/*.bats sudo bats $BATS_TESTS/20_tar_package.bats $BATS_TESTS/25_tar_plugins.bats -------------------------------------------- -To run the Java packaging tests, again inside the VM +The new packaging tests are written in Java and run on both linux and windows. On +linux (again, inside the VM) -------------------------------------------- -bash $PACKAGING_TESTS/run-tests.sh +# run the full suite +sudo bash $PACKAGING_TESTS/run-tests.sh + +# run specific test cases +sudo bash $PACKAGING_TESTS/run-tests.sh \ + org.elasticsearch.packaging.test.DefaultZipTests \ + org.elasticsearch.packaging.test.OssZipTests -------------------------------------------- -or on Windows +or on Windows, from a terminal running as Administrator -------------------------------------------- +# run the full suite powershell -File $Env:PACKAGING_TESTS/run-tests.ps1 + +# run specific test cases +powershell -File $Env:PACKAGING_TESTS/run-tests.ps1 ` + org.elasticsearch.packaging.test.DefaultZipTests ` + org.elasticsearch.packaging.test.OssZipTests -------------------------------------------- +Note that on Windows boxes when running from inside the GUI, you may have to log out and +back in to the `vagrant` user (password `vagrant`) for the environment variables that +locate the packaging tests and distributions to take effect, due to how vagrant provisions +Windows machines. + When you've made changes you want to test, keep the VM up and reload the tests and distributions inside by running (on the host) diff --git a/Vagrantfile b/Vagrantfile index 66ec60820ea..d53c80754e6 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -237,6 +237,7 @@ def linux_common(config, config.vm.provision 'markerfile', type: 'shell', inline: <<-SHELL touch /etc/is_vagrant_vm + touch /is_vagrant_vm # for consistency between linux and windows SHELL # This prevents leftovers from previous tests using the diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/vagrant/VagrantTestPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/vagrant/VagrantTestPlugin.groovy index 2e02911b7a7..6e8a5fd15ed 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/vagrant/VagrantTestPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/vagrant/VagrantTestPlugin.groovy @@ -52,6 +52,8 @@ class VagrantTestPlugin implements Plugin { static final List DISTRIBUTIONS = unmodifiableList([ 'archives:tar', 'archives:oss-tar', + 'archives:zip', + 'archives:oss-zip', 'packages:rpm', 'packages:oss-rpm', 'packages:deb', @@ -242,13 +244,27 @@ class VagrantTestPlugin implements Plugin { Task createLinuxRunnerScript = project.tasks.create('createLinuxRunnerScript', FileContentsTask) { dependsOn copyPackagingTests file "${testsDir}/run-tests.sh" - contents "java -cp \"\$PACKAGING_TESTS/*\" org.junit.runner.JUnitCore ${-> project.extensions.esvagrant.testClass}" + contents """\ + if [ "\$#" -eq 0 ]; then + test_args=( "${-> project.extensions.esvagrant.testClass}" ) + else + test_args=( "\$@" ) + fi + java -cp "\$PACKAGING_TESTS/*" org.elasticsearch.packaging.VMTestRunner "\${test_args[@]}" + """ } Task createWindowsRunnerScript = project.tasks.create('createWindowsRunnerScript', FileContentsTask) { dependsOn copyPackagingTests file "${testsDir}/run-tests.ps1" + // the use of $args rather than param() here is deliberate because the syntax for array (multivalued) parameters is likely + // a little trappy for those unfamiliar with powershell contents """\ - java -cp "\$Env:PACKAGING_TESTS/*" org.junit.runner.JUnitCore ${-> project.extensions.esvagrant.testClass} + if (\$args.Count -eq 0) { + \$testArgs = @("${-> project.extensions.esvagrant.testClass}") + } else { + \$testArgs = \$args + } + java -cp "\$Env:PACKAGING_TESTS/*" org.elasticsearch.packaging.VMTestRunner @testArgs exit \$LASTEXITCODE """ } @@ -525,9 +541,10 @@ class VagrantTestPlugin implements Plugin { if (LINUX_BOXES.contains(box)) { javaPackagingTest.command = 'ssh' - javaPackagingTest.args = ['--command', 'bash "$PACKAGING_TESTS/run-tests.sh"'] + javaPackagingTest.args = ['--command', 'sudo bash "$PACKAGING_TESTS/run-tests.sh"'] } else { javaPackagingTest.command = 'winrm' + // winrm commands run as administrator javaPackagingTest.args = ['--command', 'powershell -File "$Env:PACKAGING_TESTS/run-tests.ps1"'] } diff --git a/qa/vagrant/build.gradle b/qa/vagrant/build.gradle index 52a6bb1efb5..a8dfe89b678 100644 --- a/qa/vagrant/build.gradle +++ b/qa/vagrant/build.gradle @@ -29,9 +29,9 @@ plugins { dependencies { compile "junit:junit:${versions.junit}" compile "org.hamcrest:hamcrest-core:${versions.hamcrest}" + compile "org.hamcrest:hamcrest-library:${versions.hamcrest}" - // needs to be on the classpath for JarHell - testRuntime project(':libs:elasticsearch-core') + compile project(':libs:elasticsearch-core') // pulls in the jar built by this project and its dependencies packagingTest project(path: project.path, configuration: 'runtime') diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/PackagingTests.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/PackagingTests.java index 0b5e7a3b6e0..fa7f8e8ef78 100644 --- a/qa/vagrant/src/main/java/org/elasticsearch/packaging/PackagingTests.java +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/PackagingTests.java @@ -19,13 +19,20 @@ package org.elasticsearch.packaging; -import org.junit.Test; +import org.elasticsearch.packaging.test.OssTarTests; +import org.elasticsearch.packaging.test.OssZipTests; +import org.elasticsearch.packaging.test.DefaultTarTests; +import org.elasticsearch.packaging.test.DefaultZipTests; -/** - * This class doesn't have any tests yet - */ -public class PackagingTests { +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; - @Test - public void testDummy() {} -} +@RunWith(Suite.class) +@SuiteClasses({ + DefaultTarTests.class, + DefaultZipTests.class, + OssTarTests.class, + OssZipTests.class +}) +public class PackagingTests {} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/VMTestRunner.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/VMTestRunner.java new file mode 100644 index 00000000000..a8fd2c27707 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/VMTestRunner.java @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.packaging; + +import org.junit.runner.JUnitCore; + +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * Ensures that the current JVM is running on a virtual machine before delegating to {@link JUnitCore}. We just check for the existence + * of a special file that we create during VM provisioning. + */ +public class VMTestRunner { + public static void main(String[] args) { + if (Files.exists(Paths.get("/is_vagrant_vm"))) { + JUnitCore.main(args); + } else { + throw new RuntimeException("This filesystem does not have an expected marker file indicating it's a virtual machine. These " + + "tests should only run in a virtual machine because they're destructive."); + } + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/ArchiveTestCase.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/ArchiveTestCase.java new file mode 100644 index 00000000000..f683cb9c145 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/ArchiveTestCase.java @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.packaging.test; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +import org.elasticsearch.packaging.util.Distribution; +import org.elasticsearch.packaging.util.Installation; + +import static org.elasticsearch.packaging.util.Cleanup.cleanEverything; +import static org.elasticsearch.packaging.util.Archives.installArchive; +import static org.elasticsearch.packaging.util.Archives.verifyArchiveInstallation; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assume.assumeThat; + +/** + * Tests that apply to the archive distributions (tar, zip). To add a case for a distribution, subclass and + * override {@link ArchiveTestCase#distribution()}. These tests should be the same across all archive distributions + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public abstract class ArchiveTestCase { + + private static Installation installation; + + /** The {@link Distribution} that should be tested in this case */ + protected abstract Distribution distribution(); + + @BeforeClass + public static void cleanup() { + installation = null; + cleanEverything(); + } + + @Before + public void onlyCompatibleDistributions() { + assumeThat(distribution().packaging.compatible, is(true)); + } + + @Test + public void test10Install() { + installation = installArchive(distribution()); + verifyArchiveInstallation(installation, distribution()); + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/DefaultTarTests.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/DefaultTarTests.java new file mode 100644 index 00000000000..9b359a329c1 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/DefaultTarTests.java @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.packaging.test; + +import org.elasticsearch.packaging.util.Distribution; + +public class DefaultTarTests extends ArchiveTestCase { + + @Override + protected Distribution distribution() { + return Distribution.DEFAULT_TAR; + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/DefaultZipTests.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/DefaultZipTests.java new file mode 100644 index 00000000000..d9a6353a8c6 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/DefaultZipTests.java @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.packaging.test; + +import org.elasticsearch.packaging.util.Distribution; + +public class DefaultZipTests extends ArchiveTestCase { + + @Override + protected Distribution distribution() { + return Distribution.DEFAULT_ZIP; + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/OssTarTests.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/OssTarTests.java new file mode 100644 index 00000000000..86637fc9d48 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/OssTarTests.java @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.packaging.test; + +import org.elasticsearch.packaging.util.Distribution; + +public class OssTarTests extends ArchiveTestCase { + + @Override + protected Distribution distribution() { + return Distribution.OSS_TAR; + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/OssZipTests.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/OssZipTests.java new file mode 100644 index 00000000000..b6cd1e596a0 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/OssZipTests.java @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.packaging.test; + +import org.elasticsearch.packaging.util.Distribution; + +public class OssZipTests extends ArchiveTestCase { + + @Override + protected Distribution distribution() { + return Distribution.OSS_ZIP; + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Archives.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Archives.java new file mode 100644 index 00000000000..4a00570bf30 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Archives.java @@ -0,0 +1,239 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.packaging.util; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; + +import static org.elasticsearch.packaging.util.FileMatcher.Fileness.Directory; +import static org.elasticsearch.packaging.util.FileMatcher.Fileness.File; +import static org.elasticsearch.packaging.util.FileMatcher.file; +import static org.elasticsearch.packaging.util.FileMatcher.p644; +import static org.elasticsearch.packaging.util.FileMatcher.p660; +import static org.elasticsearch.packaging.util.FileMatcher.p755; +import static org.elasticsearch.packaging.util.FileUtils.getCurrentVersion; +import static org.elasticsearch.packaging.util.FileUtils.getDefaultArchiveInstallPath; +import static org.elasticsearch.packaging.util.FileUtils.getPackagingArchivesDir; +import static org.elasticsearch.packaging.util.FileUtils.lsGlob; + +import static org.elasticsearch.packaging.util.FileUtils.mv; +import static org.elasticsearch.packaging.util.Platforms.isDPKG; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; + +/** + * Installation and verification logic for archive distributions + */ +public class Archives { + + public static Installation installArchive(Distribution distribution) { + return installArchive(distribution, getDefaultArchiveInstallPath(), getCurrentVersion()); + } + + public static Installation installArchive(Distribution distribution, Path fullInstallPath, String version) { + final Shell sh = new Shell(); + + final Path distributionFile = getPackagingArchivesDir().resolve(distribution.filename(version)); + final Path baseInstallPath = fullInstallPath.getParent(); + final Path extractedPath = baseInstallPath.resolve("elasticsearch-" + version); + + assertThat("distribution file must exist", Files.exists(distributionFile), is(true)); + assertThat("elasticsearch must not already be installed", lsGlob(baseInstallPath, "elasticsearch*"), empty()); + + if (distribution.packaging == Distribution.Packaging.TAR) { + + if (Platforms.LINUX) { + sh.run("tar", "-C", baseInstallPath.toString(), "-xzpf", distributionFile.toString()); + } else { + throw new RuntimeException("Distribution " + distribution + " is not supported on windows"); + } + + } else if (distribution.packaging == Distribution.Packaging.ZIP) { + + if (Platforms.LINUX) { + sh.run("unzip", distributionFile.toString(), "-d", baseInstallPath.toString()); + } else { + sh.run("powershell.exe", "-Command", + "Add-Type -AssemblyName 'System.IO.Compression.Filesystem'; " + + "[IO.Compression.ZipFile]::ExtractToDirectory('" + distributionFile + "', '" + baseInstallPath + "')"); + } + + } else { + throw new RuntimeException("Distribution " + distribution + " is not a known archive type"); + } + + assertThat("archive was extracted", Files.exists(extractedPath), is(true)); + + mv(extractedPath, fullInstallPath); + + assertThat("extracted archive moved to install location", Files.exists(fullInstallPath)); + final List installations = lsGlob(baseInstallPath, "elasticsearch*"); + assertThat("only the intended installation exists", installations, hasSize(1)); + assertThat("only the intended installation exists", installations.get(0), is(fullInstallPath)); + + if (Platforms.LINUX) { + setupArchiveUsersLinux(fullInstallPath); + } + + return new Installation(fullInstallPath); + } + + private static void setupArchiveUsersLinux(Path installPath) { + final Shell sh = new Shell(); + + if (sh.runIgnoreExitCode("getent", "group", "elasticsearch").isSuccess() == false) { + if (isDPKG()) { + sh.run("addgroup", "--system", "elasticsearch"); + } else { + sh.run("groupadd", "-r", "elasticsearch"); + } + } + + if (sh.runIgnoreExitCode("id", "elasticsearch").isSuccess() == false) { + if (isDPKG()) { + sh.run("adduser", + "--quiet", + "--system", + "--no-create-home", + "--ingroup", "elasticsearch", + "--disabled-password", + "--shell", "/bin/false", + "elasticsearch"); + } else { + sh.run("useradd", + "--system", + "-M", + "--gid", "elasticsearch", + "--shell", "/sbin/nologin", + "--comment", "elasticsearch user", + "elasticsearch"); + } + } + sh.run("chown", "-R", "elasticsearch:elasticsearch", installPath.toString()); + } + + public static void verifyArchiveInstallation(Installation installation, Distribution distribution) { + // on Windows for now we leave the installation owned by the vagrant user that the tests run as. Since the vagrant account + // is a local administrator, the files really end up being owned by the local administrators group. In the future we'll + // install and run elasticesearch with a role user on Windows + final String owner = Platforms.WINDOWS + ? "BUILTIN\\Administrators" + : "elasticsearch"; + + verifyOssInstallation(installation, distribution, owner); + if (distribution.flavor == Distribution.Flavor.DEFAULT) { + verifyDefaultInstallation(installation, distribution, owner); + } + } + + private static void verifyOssInstallation(Installation es, Distribution distribution, String owner) { + Stream.of( + es.home, + es.config, + es.plugins, + es.modules, + es.logs + ).forEach(dir -> assertThat(dir, file(Directory, owner, owner, p755))); + + assertThat(Files.exists(es.data), is(false)); + assertThat(Files.exists(es.scripts), is(false)); + + assertThat(es.home.resolve("bin"), file(Directory, owner, owner, p755)); + assertThat(es.home.resolve("lib"), file(Directory, owner, owner, p755)); + assertThat(Files.exists(es.config.resolve("elasticsearch.keystore")), is(false)); + + Stream.of( + "bin/elasticsearch", + "bin/elasticsearch-env", + "bin/elasticsearch-keystore", + "bin/elasticsearch-plugin", + "bin/elasticsearch-translog" + ).forEach(executable -> { + + assertThat(es.home.resolve(executable), file(File, owner, owner, p755)); + + if (distribution.packaging == Distribution.Packaging.ZIP) { + assertThat(es.home.resolve(executable + ".bat"), file(File, owner)); + } + }); + + if (distribution.packaging == Distribution.Packaging.ZIP) { + Stream.of( + "bin/elasticsearch-service.bat", + "bin/elasticsearch-service-mgr.exe", + "bin/elasticsearch-service-x64.exe" + ).forEach(executable -> assertThat(es.home.resolve(executable), file(File, owner))); + } + + Stream.of( + "elasticsearch.yml", + "jvm.options", + "log4j2.properties" + ).forEach(config -> assertThat(es.config.resolve(config), file(File, owner, owner, p660))); + + Stream.of( + "NOTICE.txt", + "LICENSE.txt", + "README.textile" + ).forEach(doc -> assertThat(es.home.resolve(doc), file(File, owner, owner, p644))); + } + + private static void verifyDefaultInstallation(Installation es, Distribution distribution, String owner) { + + Stream.of( + "bin/elasticsearch-certgen", + "bin/elasticsearch-certutil", + "bin/elasticsearch-croneval", + "bin/elasticsearch-migrate", + "bin/elasticsearch-saml-metadata", + "bin/elasticsearch-setup-passwords", + "bin/elasticsearch-sql-cli", + "bin/elasticsearch-syskeygen", + "bin/elasticsearch-users", + "bin/x-pack-env", + "bin/x-pack-security-env", + "bin/x-pack-watcher-env" + ).forEach(executable -> { + + assertThat(es.home.resolve(executable), file(File, owner, owner, p755)); + + if (distribution.packaging == Distribution.Packaging.ZIP) { + assertThat(es.home.resolve(executable + ".bat"), file(File, owner)); + } + }); + + // at this time we only install the current version of archive distributions, but if that changes we'll need to pass + // the version through here + assertThat(es.home.resolve("bin/elasticsearch-sql-cli-" + getCurrentVersion() + ".jar"), file(File, owner, owner, p755)); + + Stream.of( + "users", + "users_roles", + "roles.yml", + "role_mapping.yml", + "log4j2.properties" + ).forEach(config -> assertThat(es.config.resolve(config), file(File, owner, owner, p660))); + } + +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Cleanup.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Cleanup.java new file mode 100644 index 00000000000..9e9150c9c18 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Cleanup.java @@ -0,0 +1,121 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.packaging.util; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.elasticsearch.packaging.util.FileUtils.getTempDir; +import static org.elasticsearch.packaging.util.FileUtils.lsGlob; +import static org.elasticsearch.packaging.util.Platforms.isAptGet; +import static org.elasticsearch.packaging.util.Platforms.isDPKG; +import static org.elasticsearch.packaging.util.Platforms.isRPM; +import static org.elasticsearch.packaging.util.Platforms.isSystemd; +import static org.elasticsearch.packaging.util.Platforms.isYUM; + +public class Cleanup { + + private static final List ELASTICSEARCH_FILES_LINUX = Arrays.asList( + "/usr/share/elasticsearch", + "/etc/elasticsearch", + "/var/lib/elasticsearch", + "/var/log/elasticsearch", + "/etc/default/elasticsearch", + "/etc/sysconfig/elasticsearch", + "/var/run/elasticsearch", + "/usr/share/doc/elasticsearch", + "/usr/lib/systemd/system/elasticsearch.conf", + "/usr/lib/tmpfiles.d/elasticsearch.conf", + "/usr/lib/sysctl.d/elasticsearch.conf" + ); + + // todo + private static final List ELASTICSEARCH_FILES_WINDOWS = Collections.emptyList(); + + public static void cleanEverything() { + final Shell sh = new Shell(); + + // kill elasticsearch processes + if (Platforms.WINDOWS) { + + // the view of processes returned by Get-Process doesn't expose command line arguments, so we use WMI here + sh.runIgnoreExitCode("powershell.exe", "-Command", + "Get-WmiObject Win32_Process | " + + "Where-Object { $_.CommandLine -Match 'org.elasticsearch.bootstrap.Elasticsearch' } | " + + "ForEach-Object { $_.Terminate() }"); + + } else { + + sh.runIgnoreExitCode("pkill", "-u", "elasticsearch"); + sh.runIgnoreExitCode("bash", "-c", + "ps aux | grep -i 'org.elasticsearch.bootstrap.Elasticsearch' | awk {'print $2'} | xargs kill -9"); + + } + + if (Platforms.LINUX) { + purgePackagesLinux(); + } + + // remove elasticsearch users + if (Platforms.LINUX) { + sh.runIgnoreExitCode("userdel", "elasticsearch"); + sh.runIgnoreExitCode("groupdel", "elasticsearch"); + } + + // delete files that may still exist + lsGlob(getTempDir(), "elasticsearch*").forEach(FileUtils::rm); + final List filesToDelete = Platforms.WINDOWS + ? ELASTICSEARCH_FILES_WINDOWS + : ELASTICSEARCH_FILES_LINUX; + filesToDelete.stream() + .map(Paths::get) + .filter(Files::exists) + .forEach(FileUtils::rm); + + // disable elasticsearch service + // todo add this for windows when adding tests for service intallation + if (Platforms.LINUX && isSystemd()) { + sh.run("systemctl", "unmask", "systemd-sysctl.service"); + } + } + + private static void purgePackagesLinux() { + final Shell sh = new Shell(); + + if (isRPM()) { + sh.runIgnoreExitCode("rpm", "--quiet", "-e", "elasticsearch", "elasticsearch-oss"); + } + + if (isYUM()) { + sh.runIgnoreExitCode("yum", "remove", "-y", "elasticsearch", "elasticsearch-oss"); + } + + if (isDPKG()) { + sh.runIgnoreExitCode("dpkg", "--purge", "elasticsearch", "elasticsearch-oss"); + } + + if (isAptGet()) { + sh.runIgnoreExitCode("apt-get", "--quiet", "--yes", "purge", "elasticsearch", "elasticsearch-oss"); + } + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Distribution.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Distribution.java new file mode 100644 index 00000000000..4f0c8751ca4 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Distribution.java @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.packaging.util; + +public enum Distribution { + + OSS_TAR(Packaging.TAR, Flavor.OSS), + OSS_ZIP(Packaging.ZIP, Flavor.OSS), + OSS_DEB(Packaging.DEB, Flavor.OSS), + OSS_RPM(Packaging.RPM, Flavor.OSS), + + DEFAULT_TAR(Packaging.TAR, Flavor.DEFAULT), + DEFAULT_ZIP(Packaging.ZIP, Flavor.DEFAULT), + DEFAULT_DEB(Packaging.DEB, Flavor.DEFAULT), + DEFAULT_RPM(Packaging.RPM, Flavor.DEFAULT); + + public final Packaging packaging; + public final Flavor flavor; + + Distribution(Packaging packaging, Flavor flavor) { + this.packaging = packaging; + this.flavor = flavor; + } + + public String filename(String version) { + return flavor.name + "-" + version + packaging.extension; + } + + public enum Packaging { + + TAR(".tar.gz", Platforms.LINUX), + ZIP(".zip", true), + DEB(".deb", Platforms.isDPKG()), + RPM(".rpm", Platforms.isRPM()); + + /** The extension of this distribution's file */ + public final String extension; + + /** Whether the distribution is intended for use on the platform the current JVM is running on */ + public final boolean compatible; + + Packaging(String extension, boolean compatible) { + this.extension = extension; + this.compatible = compatible; + } + } + + public enum Flavor { + + OSS("elasticsearch-oss"), + DEFAULT("elasticsearch"); + + public final String name; + + Flavor(String name) { + this.name = name; + } + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/FileMatcher.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/FileMatcher.java new file mode 100644 index 00000000000..9fdf6d60081 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/FileMatcher.java @@ -0,0 +1,137 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.packaging.util; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Objects; +import java.util.Set; + +import static org.elasticsearch.packaging.util.FileUtils.getBasicFileAttributes; +import static org.elasticsearch.packaging.util.FileUtils.getFileOwner; +import static org.elasticsearch.packaging.util.FileUtils.getPosixFileAttributes; + +import static java.nio.file.attribute.PosixFilePermissions.fromString; + +/** + * Asserts that a file at a path matches its status as Directory/File, and its owner. If on a posix system, also matches the permission + * set is what we expect. + * + * This class saves information about its failed matches in instance variables and so instances should not be reused + */ +public class FileMatcher extends TypeSafeMatcher { + + public enum Fileness { File, Directory } + + public static final Set p755 = fromString("rwxr-xr-x"); + public static final Set p660 = fromString("rw-rw----"); + public static final Set p644 = fromString("rw-r--r--"); + + private final Fileness fileness; + private final String owner; + private final String group; + private final Set posixPermissions; + + private String mismatch; + + public FileMatcher(Fileness fileness, String owner, String group, Set posixPermissions) { + this.fileness = Objects.requireNonNull(fileness); + this.owner = Objects.requireNonNull(owner); + this.group = group; + this.posixPermissions = posixPermissions; + } + + @Override + protected boolean matchesSafely(Path path) { + if (Files.exists(path) == false) { + mismatch = "Does not exist"; + return false; + } + + if (Platforms.WINDOWS) { + final BasicFileAttributes attributes = getBasicFileAttributes(path); + final String attributeViewOwner = getFileOwner(path); + + if (fileness.equals(Fileness.Directory) != attributes.isDirectory()) { + mismatch = "Is " + (attributes.isDirectory() ? "a directory" : "a file"); + return false; + } + + if (attributeViewOwner.contains(owner) == false) { + mismatch = "Owned by " + attributeViewOwner; + return false; + } + } else { + final PosixFileAttributes attributes = getPosixFileAttributes(path); + + if (fileness.equals(Fileness.Directory) != attributes.isDirectory()) { + mismatch = "Is " + (attributes.isDirectory() ? "a directory" : "a file"); + return false; + } + + if (owner.equals(attributes.owner().getName()) == false) { + mismatch = "Owned by " + attributes.owner().getName(); + return false; + } + + if (group != null && group.equals(attributes.group().getName()) == false) { + mismatch = "Owned by group " + attributes.group().getName(); + return false; + } + + if (posixPermissions != null && posixPermissions.equals(attributes.permissions()) == false) { + mismatch = "Has permissions " + attributes.permissions(); + return false; + } + } + + return true; + } + + @Override + public void describeMismatchSafely(Path path, Description description) { + description.appendText("path ").appendValue(path); + if (mismatch != null) { + description.appendText(mismatch); + } + } + + @Override + public void describeTo(Description description) { + description.appendValue("file/directory: ").appendValue(fileness) + .appendText(" with owner ").appendValue(owner) + .appendText(" with group ").appendValue(group) + .appendText(" with posix permissions ").appendValueList("[", ",", "]", posixPermissions); + } + + public static FileMatcher file(Fileness fileness, String owner) { + return file(fileness, owner, null, null); + } + + public static FileMatcher file(Fileness fileness, String owner, String group, Set permissions) { + return new FileMatcher(fileness, owner, group, permissions); + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/FileUtils.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/FileUtils.java new file mode 100644 index 00000000000..ad826675244 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/FileUtils.java @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.packaging.util; + +import org.elasticsearch.core.internal.io.IOUtils; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileOwnerAttributeView; +import java.nio.file.attribute.PosixFileAttributes; +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsNot.not; +import static org.hamcrest.text.IsEmptyString.isEmptyOrNullString; + +/** + * Wrappers and convenience methods for common filesystem operations + */ +public class FileUtils { + + public static List lsGlob(Path directory, String glob) { + List paths = new ArrayList<>(); + try (DirectoryStream stream = Files.newDirectoryStream(directory, glob)) { + + for (Path path : stream) { + paths.add(path); + } + return paths; + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static void rm(Path... paths) { + try { + IOUtils.rm(paths); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static Path mv(Path source, Path target) { + try { + return Files.move(source, target); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static String slurp(Path file) { + try { + return String.join("\n", Files.readAllLines(file)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Gets the owner of a file in a way that should be supported by all filesystems that have a concept of file owner + */ + public static String getFileOwner(Path path) { + try { + FileOwnerAttributeView view = Files.getFileAttributeView(path, FileOwnerAttributeView.class); + return view.getOwner().getName(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Gets attributes that are supported by all filesystems + */ + public static BasicFileAttributes getBasicFileAttributes(Path path) { + try { + return Files.readAttributes(path, BasicFileAttributes.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Gets attributes that are supported by posix filesystems + */ + public static PosixFileAttributes getPosixFileAttributes(Path path) { + try { + return Files.readAttributes(path, PosixFileAttributes.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + // vagrant creates /tmp for us in windows so we use that to avoid long paths + public static Path getTempDir() { + return Paths.get("/tmp"); + } + + public static Path getDefaultArchiveInstallPath() { + return getTempDir().resolve("elasticsearch"); + } + + public static String getCurrentVersion() { + return slurp(getPackagingArchivesDir().resolve("version")); + } + + public static Path getPackagingArchivesDir() { + String fromEnv = System.getenv("PACKAGING_ARCHIVES"); + assertThat(fromEnv, not(isEmptyOrNullString())); + return Paths.get(fromEnv); + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Installation.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Installation.java new file mode 100644 index 00000000000..d231762d062 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Installation.java @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.packaging.util; + +import java.nio.file.Path; + +/** + * Represents an installation of Elasticsearch + */ +public class Installation { + + public final Path home; + public final Path config; + public final Path data; + public final Path logs; + public final Path plugins; + public final Path modules; + public final Path scripts; + + public Installation(Path home, Path config, Path data, Path logs, Path plugins, Path modules, Path scripts) { + this.home = home; + this.config = config; + this.data = data; + this.logs = logs; + this.plugins = plugins; + this.modules = modules; + this.scripts = scripts; + } + + public Installation(Path home) { + this( + home, + home.resolve("config"), + home.resolve("data"), + home.resolve("logs"), + home.resolve("plugins"), + home.resolve("modules"), + home.resolve("scripts") + ); + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Platforms.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Platforms.java new file mode 100644 index 00000000000..230af8efc2d --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Platforms.java @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.packaging.util; + +public class Platforms { + public static final String OS_NAME = System.getProperty("os.name"); + public static final boolean LINUX = OS_NAME.startsWith("Linux"); + public static final boolean WINDOWS = OS_NAME.startsWith("Windows"); + + public static boolean isDPKG() { + if (WINDOWS) { + return false; + } + return new Shell().runIgnoreExitCode("which", "dpkg").isSuccess(); + } + + public static boolean isAptGet() { + if (WINDOWS) { + return false; + } + return new Shell().runIgnoreExitCode("which", "apt-get").isSuccess(); + } + + public static boolean isRPM() { + if (WINDOWS) { + return false; + } + return new Shell().runIgnoreExitCode("which", "rpm").isSuccess(); + } + + public static boolean isYUM() { + if (WINDOWS) { + return false; + } + return new Shell().runIgnoreExitCode("which", "yum").isSuccess(); + } + + public static boolean isSystemd() { + if (WINDOWS) { + return false; + } + return new Shell().runIgnoreExitCode("which", "systemctl").isSuccess(); + } + + public static boolean isSysVInit() { + if (WINDOWS) { + return false; + } + return new Shell().runIgnoreExitCode("which", "service").isSuccess(); + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Shell.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Shell.java new file mode 100644 index 00000000000..3adc0b62e04 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Shell.java @@ -0,0 +1,193 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.packaging.util; + +import org.elasticsearch.common.SuppressForbidden; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static java.util.Collections.emptyMap; + +/** + * Wrapper to run shell commands and collect their outputs in a less verbose way + */ +public class Shell { + + final Map env; + final Path workingDirectory; + + public Shell() { + this(emptyMap(), null); + } + + public Shell(Map env) { + this(env, null); + } + + public Shell(Path workingDirectory) { + this(emptyMap(), workingDirectory); + } + + public Shell(Map env, Path workingDirectory) { + this.env = new HashMap<>(env); + this.workingDirectory = workingDirectory; + } + + public Result run(String... command) { + Result result = runIgnoreExitCode(command); + if (result.isSuccess() == false) { + throw new RuntimeException("Command was not successful: [" + String.join(" ", command) + "] result: " + result.toString()); + } + return result; + } + + public Result runIgnoreExitCode(String... command) { + ProcessBuilder builder = new ProcessBuilder(); + builder.command(command); + + if (workingDirectory != null) { + setWorkingDirectory(builder, workingDirectory); + } + + if (env != null && env.isEmpty() == false) { + for (Map.Entry entry : env.entrySet()) { + builder.environment().put(entry.getKey(), entry.getValue()); + } + } + + try { + + Process process = builder.start(); + + StringBuilder stdout = new StringBuilder(); + StringBuilder stderr = new StringBuilder(); + + Thread stdoutThread = new Thread(new StreamCollector(process.getInputStream(), stdout)); + Thread stderrThread = new Thread(new StreamCollector(process.getErrorStream(), stderr)); + + stdoutThread.start(); + stderrThread.start(); + + stdoutThread.join(); + stderrThread.join(); + + int exitCode = process.waitFor(); + + return new Result(exitCode, stdout.toString(), stderr.toString()); + + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + @SuppressForbidden(reason = "ProcessBuilder expects java.io.File") + private static void setWorkingDirectory(ProcessBuilder builder, Path path) { + builder.directory(path.toFile()); + } + + public String toString() { + return new StringBuilder() + .append("<") + .append(this.getClass().getName()) + .append(" ") + .append("env = [") + .append(env) + .append("]") + .append("workingDirectory = [") + .append(workingDirectory) + .append("]") + .append(">") + .toString(); + } + + public static class Result { + public final int exitCode; + public final String stdout; + public final String stderr; + + public Result(int exitCode, String stdout, String stderr) { + this.exitCode = exitCode; + this.stdout = stdout; + this.stderr = stderr; + } + + public boolean isSuccess() { + return exitCode == 0; + } + + public String toString() { + return new StringBuilder() + .append("<") + .append(this.getClass().getName()) + .append(" ") + .append("exitCode = [") + .append(exitCode) + .append("]") + .append(" ") + .append("stdout = [") + .append(stdout) + .append("]") + .append(" ") + .append("stderr = [") + .append(stderr) + .append("]") + .append(">") + .toString(); + } + } + + private static class StreamCollector implements Runnable { + private final InputStream input; + private final Appendable appendable; + + StreamCollector(InputStream input, Appendable appendable) { + this.input = Objects.requireNonNull(input); + this.appendable = Objects.requireNonNull(appendable); + } + + public void run() { + try { + + BufferedReader reader = new BufferedReader(reader(input)); + String line; + + while ((line = reader.readLine()) != null) { + appendable.append(line); + appendable.append("\n"); + } + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @SuppressForbidden(reason = "the system's default character set is a best guess of what subprocesses will use") + private static InputStreamReader reader(InputStream inputStream) { + return new InputStreamReader(inputStream); + } + } +}