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.tool.coprocessor;
019
020import static org.junit.Assert.assertEquals;
021import static org.junit.Assert.assertTrue;
022import static org.mockito.Mockito.doReturn;
023import static org.mockito.Mockito.mock;
024
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.OutputStream;
028import java.nio.file.Files;
029import java.nio.file.Path;
030import java.nio.file.Paths;
031import java.util.ArrayList;
032import java.util.List;
033import java.util.Optional;
034import java.util.jar.JarOutputStream;
035import java.util.regex.Pattern;
036import java.util.zip.ZipEntry;
037import org.apache.hadoop.hbase.HBaseClassTestRule;
038import org.apache.hadoop.hbase.HBaseConfiguration;
039import org.apache.hadoop.hbase.HRegionInfo;
040import org.apache.hadoop.hbase.HTableDescriptor;
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 })
057@SuppressWarnings("deprecation")
058public class CoprocessorValidatorTest {
059  @ClassRule
060  public static final HBaseClassTestRule CLASS_RULE =
061    HBaseClassTestRule.forClass(CoprocessorValidatorTest.class);
062
063  private CoprocessorValidator validator;
064
065  public CoprocessorValidatorTest() {
066    validator = new CoprocessorValidator();
067    validator.setConf(HBaseConfiguration.create());
068  }
069
070  private static ClassLoader getClassLoader() {
071    return CoprocessorValidatorTest.class.getClassLoader();
072  }
073
074  private static String getFullClassName(String className) {
075    return CoprocessorValidatorTest.class.getName() + "$" + className;
076  }
077
078  private List<CoprocessorViolation> validateClass(String className) {
079    ClassLoader classLoader = getClass().getClassLoader();
080    return validateClass(classLoader, className);
081  }
082
083  private List<CoprocessorViolation> validateClass(ClassLoader classLoader, String className) {
084    List<String> classNames = Lists.newArrayList(getFullClassName(className));
085    List<CoprocessorViolation> violations = new ArrayList<>();
086
087    validator.validateClasses(classLoader, classNames, violations);
088
089    return violations;
090  }
091
092  /*
093   * In this test case, we are try to load a not-existent class.
094   */
095  @Test
096  public void testNoSuchClass() throws IOException {
097    List<CoprocessorViolation> violations = validateClass("NoSuchClass");
098    assertEquals(1, violations.size());
099
100    CoprocessorViolation violation = violations.get(0);
101    assertEquals(getFullClassName("NoSuchClass"), violation.getClassName());
102    assertEquals(Severity.ERROR, violation.getSeverity());
103
104    String stackTrace = Throwables.getStackTraceAsString(violation.getThrowable());
105    assertTrue(stackTrace.contains("java.lang.ClassNotFoundException: "
106      + "org.apache.hadoop.hbase.tool.coprocessor.CoprocessorValidatorTest$NoSuchClass"));
107  }
108
109  /*
110   * In this test case, we are validating MissingClass coprocessor, which references a missing
111   * class. With a special classloader, we prevent that class to be loaded at runtime. It simulates
112   * similar cases where a class is no more on our classpath. E.g.
113   * org.apache.hadoop.hbase.regionserver.wal.WALEdit was moved to org.apache.hadoop.hbase.wal, so
114   * 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 =
144      validateClass(missingClassClassLoader, "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 preCreateTable(ObserverContext<MasterCoprocessorEnvironment> ctx,
164      HTableDescriptor desc, HRegionInfo[] regions) 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 = Lists.newArrayList(coprocessorDescriptor);
191    doReturn(coprocessorDescriptors).when(tableDescriptor).getCoprocessorDescriptors();
192
193    doReturn(getFullClassName(className)).when(coprocessorDescriptor).getClassName();
194    doReturn(Optional.ofNullable(jarFile)).when(coprocessorDescriptor).getJarPath();
195
196    List<CoprocessorViolation> violations = new ArrayList<>();
197
198    validator.validateTables(getClassLoader(), admin, pattern, violations);
199
200    return violations;
201  }
202
203  @Test
204  public void testTableNoSuchClass() throws IOException {
205    List<CoprocessorViolation> violations = validateTable(null, "NoSuchClass");
206    assertEquals(1, violations.size());
207
208    CoprocessorViolation violation = violations.get(0);
209    assertEquals(getFullClassName("NoSuchClass"), violation.getClassName());
210    assertEquals(Severity.ERROR, violation.getSeverity());
211
212    String stackTrace = Throwables.getStackTraceAsString(violation.getThrowable());
213    assertTrue(stackTrace.contains("java.lang.ClassNotFoundException: "
214      + "org.apache.hadoop.hbase.tool.coprocessor.CoprocessorValidatorTest$NoSuchClass"));
215  }
216
217  @Test
218  public void testTableMissingJar() throws IOException {
219    List<CoprocessorViolation> violations = validateTable("no such file", "NoSuchClass");
220    assertEquals(1, violations.size());
221
222    CoprocessorViolation violation = violations.get(0);
223    assertEquals(getFullClassName("NoSuchClass"), violation.getClassName());
224    assertEquals(Severity.ERROR, violation.getSeverity());
225    assertTrue(violation.getMessage().contains("could not validate jar file 'no such file'"));
226  }
227
228  @Test
229  public void testTableValidJar() throws IOException {
230    Path outputDirectory = Paths.get("target", "test-classes");
231    String className = getFullClassName("ObsoleteMethodObserver");
232    Path classFile = Paths.get(className.replace('.', '/') + ".class");
233    Path fullClassFile = outputDirectory.resolve(classFile);
234
235    Path tempJarFile = Files.createTempFile("coprocessor-validator-test-", ".jar");
236
237    try {
238      try (OutputStream fileStream = Files.newOutputStream(tempJarFile);
239        JarOutputStream jarStream = new JarOutputStream(fileStream);
240        InputStream classStream = Files.newInputStream(fullClassFile)) {
241        ZipEntry entry = new ZipEntry(classFile.toString());
242        jarStream.putNextEntry(entry);
243
244        ByteStreams.copy(classStream, jarStream);
245      }
246
247      String tempJarFileUri = tempJarFile.toUri().toString();
248
249      List<CoprocessorViolation> violations =
250        validateTable(tempJarFileUri, "ObsoleteMethodObserver");
251      assertEquals(1, violations.size());
252
253      CoprocessorViolation violation = violations.get(0);
254      assertEquals(getFullClassName("ObsoleteMethodObserver"), violation.getClassName());
255      assertEquals(Severity.WARNING, violation.getSeverity());
256      assertTrue(violation.getMessage().contains("was removed from new coprocessor API"));
257    } finally {
258      Files.delete(tempJarFile);
259    }
260  }
261}