001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase;
019
020import static org.junit.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.assertFalse;
022import static org.junit.jupiter.api.Assertions.assertTrue;
023
024import java.io.File;
025import java.io.IOException;
026import java.nio.charset.StandardCharsets;
027import java.nio.file.Files;
028import java.nio.file.Path;
029import java.nio.file.Paths;
030import java.util.Optional;
031import java.util.function.Consumer;
032import org.apache.hadoop.hbase.client.AsyncAdmin;
033import org.apache.hadoop.hbase.client.AsyncConnection;
034import org.apache.hadoop.hbase.client.ConnectionFactory;
035import org.apache.hadoop.hbase.coprocessor.protobuf.generated.ShellExecEndpoint.ShellExecRequest;
036import org.apache.hadoop.hbase.coprocessor.protobuf.generated.ShellExecEndpoint.ShellExecResponse;
037import org.apache.hadoop.hbase.coprocessor.protobuf.generated.ShellExecEndpoint.ShellExecService;
038import org.apache.hadoop.hbase.testclassification.MediumTests;
039import org.junit.jupiter.api.AfterAll;
040import org.junit.jupiter.api.AfterEach;
041import org.junit.jupiter.api.BeforeAll;
042import org.junit.jupiter.api.BeforeEach;
043import org.junit.jupiter.api.Tag;
044import org.junit.jupiter.api.Test;
045
046/**
047 * Test for the {@link ShellExecEndpointCoprocessor}.
048 */
049@Tag(MediumTests.TAG)
050public class TestShellExecEndpointCoprocessor {
051
052  private static HBaseTestingUtil testingUtility;
053  private AsyncConnection conn;
054
055  @BeforeAll
056  public static void setUp() throws Exception {
057    testingUtility = new HBaseTestingUtil();
058    testingUtility.getConfiguration().set("hbase.coprocessor.master.classes",
059      ShellExecEndpointCoprocessor.class.getName());
060    testingUtility.startMiniCluster();
061  }
062
063  @AfterAll
064  public static void tearDown() throws Exception {
065    testingUtility.shutdownMiniCluster();
066  }
067
068  @BeforeEach
069  public void setUpConnection() throws Exception {
070    conn = ConnectionFactory.createAsyncConnection(testingUtility.getConfiguration()).get();
071  }
072
073  @AfterEach
074  public void tearDownConnection() throws IOException {
075    if (conn != null) {
076      conn.close();
077    }
078  }
079
080  @Test
081  public void testShellExecUnspecified() {
082    testShellExecForeground(b -> {
083    });
084  }
085
086  @Test
087  public void testShellExecForeground() {
088    testShellExecForeground(b -> b.setAwaitResponse(true));
089  }
090
091  private void testShellExecForeground(final Consumer<ShellExecRequest.Builder> consumer) {
092    final AsyncAdmin admin = conn.getAdmin();
093
094    final String command = "echo -n \"hello world\"";
095    final ShellExecRequest.Builder builder = ShellExecRequest.newBuilder().setCommand(command);
096    consumer.accept(builder);
097    final ShellExecResponse resp =
098      admin.<ShellExecService.Stub, ShellExecResponse> coprocessorService(ShellExecService::newStub,
099        (stub, controller, callback) -> stub.shellExec(controller, builder.build(), callback))
100        .join();
101    assertEquals(0, resp.getExitCode());
102    assertEquals("hello world", resp.getStdout());
103  }
104
105  @Test
106  public void testShellExecBackground() throws IOException {
107    final AsyncAdmin admin = conn.getAdmin();
108
109    final File testDataDir = ensureTestDataDirExists(testingUtility);
110    final File testFile = new File(testDataDir, "shell_exec_background.txt");
111    assertTrue(testFile.createNewFile());
112    assertEquals(0, testFile.length());
113
114    final String command = "echo \"hello world\" >> " + testFile.getAbsolutePath();
115    final ShellExecRequest req =
116      ShellExecRequest.newBuilder().setCommand(command).setAwaitResponse(false).build();
117    final ShellExecResponse resp =
118      admin.<ShellExecService.Stub, ShellExecResponse> coprocessorService(ShellExecService::newStub,
119        (stub, controller, callback) -> stub.shellExec(controller, req, callback)).join();
120
121    assertFalse(resp.hasExitCode(), "the response from a background task should have no exit code");
122    assertFalse(resp.hasStdout(), "the response from a background task should have no stdout");
123    assertFalse(resp.hasStderr(), "the response from a background task should have no stderr");
124
125    Waiter.waitFor(conn.getConfiguration(), 5_000, () -> testFile.length() > 0);
126    final String content =
127      new String(Files.readAllBytes(testFile.toPath()), StandardCharsets.UTF_8).trim();
128    assertEquals("hello world", content);
129  }
130
131  private static File ensureTestDataDirExists(final HBaseTestingUtil testingUtility)
132    throws IOException {
133    final Path testDataDir = Optional.of(testingUtility).map(HBaseTestingUtil::getDataTestDir)
134      .map(Object::toString).map(Paths::get)
135      .orElseThrow(() -> new RuntimeException("Unable to locate temp directory path."));
136    final File testDataDirFile = Files.createDirectories(testDataDir).toFile();
137    assertTrue(testDataDirFile.exists());
138    return testDataDirFile;
139  }
140}