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