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