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