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 */
018
019package org.apache.hadoop.hbase.tool.coprocessor;
020
021import static org.junit.Assert.assertEquals;
022import static org.junit.Assert.assertTrue;
023import static org.mockito.Mockito.doReturn;
024import static org.mockito.Mockito.mock;
025
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.OutputStream;
029import java.nio.file.Files;
030import java.nio.file.Path;
031import java.nio.file.Paths;
032import java.util.ArrayList;
033import java.util.List;
034import java.util.Optional;
035import java.util.jar.JarOutputStream;
036import java.util.regex.Pattern;
037import java.util.zip.ZipEntry;
038import org.apache.hadoop.hbase.HBaseClassTestRule;
039import org.apache.hadoop.hbase.HBaseConfiguration;
040import org.apache.hadoop.hbase.TableName;
041import org.apache.hadoop.hbase.client.Admin;
042import org.apache.hadoop.hbase.client.CoprocessorDescriptor;
043import org.apache.hadoop.hbase.client.TableDescriptor;
044import org.apache.hadoop.hbase.coprocessor.MasterCoprocessorEnvironment;
045import org.apache.hadoop.hbase.coprocessor.ObserverContext;
046import org.apache.hadoop.hbase.testclassification.SmallTests;
047import org.apache.hadoop.hbase.tool.coprocessor.CoprocessorViolation.Severity;
048import org.junit.ClassRule;
049import org.junit.Test;
050import org.junit.experimental.categories.Category;
051
052import org.apache.hbase.thirdparty.com.google.common.base.Throwables;
053import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
054import org.apache.hbase.thirdparty.com.google.common.io.ByteStreams;
055
056@Category({ SmallTests.class })
057public class CoprocessorValidatorTest {
058  @ClassRule
059  public static final HBaseClassTestRule CLASS_RULE =
060      HBaseClassTestRule.forClass(CoprocessorValidatorTest.class);
061
062  private CoprocessorValidator validator;
063
064  public CoprocessorValidatorTest() {
065    validator = new CoprocessorValidator();
066    validator.setConf(HBaseConfiguration.create());
067  }
068
069  private static ClassLoader getClassLoader() {
070    return CoprocessorValidatorTest.class.getClassLoader();
071  }
072
073  private static String getFullClassName(String className) {
074    return CoprocessorValidatorTest.class.getName() + "$" + className;
075  }
076
077  private List<CoprocessorViolation> validateClass(String className) {
078    ClassLoader classLoader = getClass().getClassLoader();
079    return validateClass(classLoader, className);
080  }
081
082  private List<CoprocessorViolation> validateClass(ClassLoader classLoader, String className) {
083    List<String> classNames = Lists.newArrayList(getFullClassName(className));
084    List<CoprocessorViolation> violations = new ArrayList<>();
085
086    validator.validateClasses(classLoader, classNames, violations);
087
088    return violations;
089  }
090
091  /*
092   * In this test case, we are try to load a not-existent class.
093   */
094  @Test
095  public void testNoSuchClass() throws IOException {
096    List<CoprocessorViolation> violations = validateClass("NoSuchClass");
097    assertEquals(1, violations.size());
098
099    CoprocessorViolation violation = violations.get(0);
100    assertEquals(getFullClassName("NoSuchClass"), violation.getClassName());
101    assertEquals(Severity.ERROR, violation.getSeverity());
102
103    String stackTrace = Throwables.getStackTraceAsString(violation.getThrowable());
104    assertTrue(stackTrace.contains("java.lang.ClassNotFoundException: " +
105        "org.apache.hadoop.hbase.tool.coprocessor.CoprocessorValidatorTest$NoSuchClass"));
106  }
107
108  /*
109   * In this test case, we are validating MissingClass coprocessor, which
110   * references a missing class. With a special classloader, we prevent that
111   * class to be loaded at runtime. It simulates similar cases where a class
112   * is no more on our classpath.
113   * E.g. org.apache.hadoop.hbase.regionserver.wal.WALEdit was moved to
114   * org.apache.hadoop.hbase.wal, so class loading will fail on 2.0.
115   */
116  private static class MissingClass {
117  }
118
119  @SuppressWarnings("unused")
120  private static class MissingClassObserver {
121    public void method(MissingClass missingClass) {
122    }
123  }
124
125  private static class MissingClassClassLoader extends ClassLoader {
126    public MissingClassClassLoader() {
127      super(getClassLoader());
128    }
129
130    @Override
131    public Class<?> loadClass(String name) throws ClassNotFoundException {
132      if (name.equals(getFullClassName("MissingClass"))) {
133        throw new ClassNotFoundException(name);
134      }
135
136      return super.findClass(name);
137    }
138  }
139
140  @Test
141  public void testMissingClass() throws IOException {
142    MissingClassClassLoader missingClassClassLoader = new MissingClassClassLoader();
143    List<CoprocessorViolation> violations = validateClass(missingClassClassLoader,
144        "MissingClassObserver");
145    assertEquals(1, violations.size());
146
147    CoprocessorViolation violation = violations.get(0);
148    assertEquals(getFullClassName("MissingClassObserver"), violation.getClassName());
149    assertEquals(Severity.ERROR, violation.getSeverity());
150
151    String stackTrace = Throwables.getStackTraceAsString(violation.getThrowable());
152    assertTrue(stackTrace.contains("java.lang.ClassNotFoundException: " +
153        "org.apache.hadoop.hbase.tool.coprocessor.CoprocessorValidatorTest$MissingClass"));
154  }
155
156  /**
157   * ObsoleteMethod coprocessor implements preCreateTable method which has HRegionInfo parameters.
158   * In our current implementation, we pass only RegionInfo parameters, so this method won't be
159   * called by HBase at all.
160   */
161  @SuppressWarnings("unused")
162  private static class ObsoleteMethodObserver /* implements MasterObserver */ {
163    public void preEnableTableHandler(ObserverContext<MasterCoprocessorEnvironment> ctx,
164      TableName tablName) throws IOException {
165    }
166  }
167
168  @Test
169  public void testObsoleteMethod() throws IOException {
170    List<CoprocessorViolation> violations = validateClass("ObsoleteMethodObserver");
171    assertEquals(1, violations.size());
172
173    CoprocessorViolation violation = violations.get(0);
174    assertEquals(Severity.WARNING, violation.getSeverity());
175    assertEquals(getFullClassName("ObsoleteMethodObserver"), violation.getClassName());
176    assertTrue(violation.getMessage().contains("was removed from new coprocessor API"));
177  }
178
179  private List<CoprocessorViolation> validateTable(String jarFile, String className)
180      throws IOException {
181    Pattern pattern = Pattern.compile(".*");
182
183    Admin admin = mock(Admin.class);
184
185    TableDescriptor tableDescriptor = mock(TableDescriptor.class);
186    List<TableDescriptor> tableDescriptors = Lists.newArrayList(tableDescriptor);
187    doReturn(tableDescriptors).when(admin).listTableDescriptors(pattern);
188
189    CoprocessorDescriptor coprocessorDescriptor = mock(CoprocessorDescriptor.class);
190    List<CoprocessorDescriptor> coprocessorDescriptors =
191        Lists.newArrayList(coprocessorDescriptor);
192    doReturn(coprocessorDescriptors).when(tableDescriptor).getCoprocessorDescriptors();
193
194    doReturn(getFullClassName(className)).when(coprocessorDescriptor).getClassName();
195    doReturn(Optional.ofNullable(jarFile)).when(coprocessorDescriptor).getJarPath();
196
197    List<CoprocessorViolation> violations = new ArrayList<>();
198
199    validator.validateTables(getClassLoader(), admin, pattern, violations);
200
201    return violations;
202  }
203
204  @Test
205  public void testTableNoSuchClass() throws IOException {
206    List<CoprocessorViolation> violations = validateTable(null, "NoSuchClass");
207    assertEquals(1, violations.size());
208
209    CoprocessorViolation violation = violations.get(0);
210    assertEquals(getFullClassName("NoSuchClass"), violation.getClassName());
211    assertEquals(Severity.ERROR, violation.getSeverity());
212
213    String stackTrace = Throwables.getStackTraceAsString(violation.getThrowable());
214    assertTrue(stackTrace.contains("java.lang.ClassNotFoundException: " +
215        "org.apache.hadoop.hbase.tool.coprocessor.CoprocessorValidatorTest$NoSuchClass"));
216  }
217
218  @Test
219  public void testTableMissingJar() throws IOException {
220    List<CoprocessorViolation> violations = validateTable("no such file", "NoSuchClass");
221    assertEquals(1, violations.size());
222
223    CoprocessorViolation violation = violations.get(0);
224    assertEquals(getFullClassName("NoSuchClass"), violation.getClassName());
225    assertEquals(Severity.ERROR, violation.getSeverity());
226    assertTrue(violation.getMessage().contains("could not validate jar file 'no such file'"));
227  }
228
229  @Test
230  public void testTableValidJar() throws IOException {
231    Path outputDirectory = Paths.get("target", "test-classes");
232    String className = getFullClassName("ObsoleteMethodObserver");
233    Path classFile = Paths.get(className.replace('.', '/') + ".class");
234    Path fullClassFile = outputDirectory.resolve(classFile);
235
236    Path tempJarFile = Files.createTempFile("coprocessor-validator-test-", ".jar");
237
238    try {
239      try (OutputStream fileStream = Files.newOutputStream(tempJarFile);
240          JarOutputStream jarStream = new JarOutputStream(fileStream);
241          InputStream classStream = Files.newInputStream(fullClassFile)) {
242        ZipEntry entry = new ZipEntry(classFile.toString());
243        jarStream.putNextEntry(entry);
244
245        ByteStreams.copy(classStream, jarStream);
246      }
247
248      String tempJarFileUri = tempJarFile.toUri().toString();
249
250      List<CoprocessorViolation> violations =
251          validateTable(tempJarFileUri, "ObsoleteMethodObserver");
252      assertEquals(1, violations.size());
253
254      CoprocessorViolation violation = violations.get(0);
255      assertEquals(getFullClassName("ObsoleteMethodObserver"), violation.getClassName());
256      assertEquals(Severity.WARNING, violation.getSeverity());
257      assertTrue(violation.getMessage().contains("was removed from new coprocessor API"));
258    } finally {
259      Files.delete(tempJarFile);
260    }
261  }
262}