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.rest;
019
020import static org.junit.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.assertNotNull;
022import static org.junit.jupiter.api.Assertions.assertNull;
023import static org.junit.jupiter.api.Assertions.assertTrue;
024
025import com.fasterxml.jackson.databind.ObjectMapper;
026import java.io.ByteArrayInputStream;
027import java.io.IOException;
028import java.io.StringWriter;
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.HashMap;
032import java.util.List;
033import java.util.Map;
034import javax.xml.bind.JAXBContext;
035import javax.xml.bind.JAXBException;
036import org.apache.hadoop.conf.Configuration;
037import org.apache.hadoop.hbase.HBaseTestingUtil;
038import org.apache.hadoop.hbase.NamespaceDescriptor;
039import org.apache.hadoop.hbase.TableName;
040import org.apache.hadoop.hbase.client.Admin;
041import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
042import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
043import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
044import org.apache.hadoop.hbase.rest.client.Client;
045import org.apache.hadoop.hbase.rest.client.Cluster;
046import org.apache.hadoop.hbase.rest.client.Response;
047import org.apache.hadoop.hbase.rest.model.NamespacesInstanceModel;
048import org.apache.hadoop.hbase.rest.model.TableListModel;
049import org.apache.hadoop.hbase.rest.model.TableModel;
050import org.apache.hadoop.hbase.rest.model.TestNamespacesInstanceModel;
051import org.apache.hadoop.hbase.testclassification.MediumTests;
052import org.apache.hadoop.hbase.testclassification.RestTests;
053import org.apache.hadoop.hbase.util.Bytes;
054import org.apache.http.Header;
055import org.junit.jupiter.api.AfterAll;
056import org.junit.jupiter.api.BeforeAll;
057import org.junit.jupiter.api.Tag;
058import org.junit.jupiter.api.Test;
059
060import org.apache.hbase.thirdparty.com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
061import org.apache.hbase.thirdparty.javax.ws.rs.core.MediaType;
062
063@Tag(RestTests.TAG)
064@Tag(MediumTests.TAG)
065public class TestNamespacesInstanceResource {
066
067  private static final String NAMESPACE1 = "TestNamespacesInstanceResource1";
068  private static final Map<String, String> NAMESPACE1_PROPS = new HashMap<>();
069  private static final String NAMESPACE2 = "TestNamespacesInstanceResource2";
070  private static final Map<String, String> NAMESPACE2_PROPS = new HashMap<>();
071  private static final String NAMESPACE3 = "TestNamespacesInstanceResource3";
072  private static final Map<String, String> NAMESPACE3_PROPS = new HashMap<>();
073  private static final String NAMESPACE4 = "TestNamespacesInstanceResource4";
074  private static final Map<String, String> NAMESPACE4_PROPS = new HashMap<>();
075
076  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
077  private static final HBaseRESTTestingUtility REST_TEST_UTIL = new HBaseRESTTestingUtility();
078  private static Client client;
079  private static JAXBContext context;
080  private static Configuration conf;
081  private static TestNamespacesInstanceModel testNamespacesInstanceModel;
082  protected static ObjectMapper jsonMapper;
083
084  @BeforeAll
085  public static void setUpBeforeClass() throws Exception {
086    conf = TEST_UTIL.getConfiguration();
087    TEST_UTIL.startMiniCluster();
088    REST_TEST_UTIL.startServletContainer(conf);
089    client = new Client(new Cluster().add("localhost", REST_TEST_UTIL.getServletPort()));
090    testNamespacesInstanceModel = new TestNamespacesInstanceModel();
091    context = JAXBContext.newInstance(NamespacesInstanceModel.class, TableListModel.class);
092    jsonMapper = new JacksonJaxbJsonProvider().locateMapper(NamespacesInstanceModel.class,
093      MediaType.APPLICATION_JSON_TYPE);
094    NAMESPACE1_PROPS.put("key1", "value1");
095    NAMESPACE2_PROPS.put("key2a", "value2a");
096    NAMESPACE2_PROPS.put("key2b", "value2b");
097    NAMESPACE3_PROPS.put("key3", "value3");
098    NAMESPACE4_PROPS.put("key4a", "value4a");
099    NAMESPACE4_PROPS.put("key4b", "value4b");
100  }
101
102  @AfterAll
103  public static void tearDownAfterClass() throws Exception {
104    REST_TEST_UTIL.shutdownServletContainer();
105    TEST_UTIL.shutdownMiniCluster();
106  }
107
108  private static byte[] toXML(NamespacesInstanceModel model) throws JAXBException {
109    StringWriter writer = new StringWriter();
110    context.createMarshaller().marshal(model, writer);
111    return Bytes.toBytes(writer.toString());
112  }
113
114  @SuppressWarnings("unchecked")
115  private static <T> T fromXML(byte[] content, Class<T> clazz) throws JAXBException {
116    JAXBContext jaxbContext = JAXBContext.newInstance(clazz);
117    return (T) jaxbContext.createUnmarshaller().unmarshal(new ByteArrayInputStream(content));
118  }
119
120  private NamespaceDescriptor findNamespace(Admin admin, String namespaceName) throws IOException {
121    NamespaceDescriptor[] nd = admin.listNamespaceDescriptors();
122    for (NamespaceDescriptor namespaceDescriptor : nd) {
123      if (namespaceDescriptor.getName().equals(namespaceName)) {
124        return namespaceDescriptor;
125      }
126    }
127    return null;
128  }
129
130  private void checkNamespaceProperties(NamespaceDescriptor nd, Map<String, String> testProps) {
131    checkNamespaceProperties(nd.getConfiguration(), testProps);
132  }
133
134  private void checkNamespaceProperties(Map<String, String> namespaceProps,
135    Map<String, String> testProps) {
136    assertEquals(namespaceProps.size(), testProps.size());
137    for (String key : testProps.keySet()) {
138      assertEquals(testProps.get(key), namespaceProps.get(key));
139    }
140  }
141
142  private void checkNamespaceTables(List<TableModel> namespaceTables, List<String> testTables) {
143    assertEquals(namespaceTables.size(), testTables.size());
144    for (TableModel namespaceTable : namespaceTables) {
145      String tableName = namespaceTable.getName();
146      assertTrue(testTables.contains(tableName));
147    }
148  }
149
150  @Test
151  public void testCannotDeleteDefaultAndHbaseNamespaces() throws IOException {
152    String defaultPath = "/namespaces/default";
153    String hbasePath = "/namespaces/hbase";
154    Response response;
155
156    // Check that doesn't exist via non-REST call.
157    Admin admin = TEST_UTIL.getAdmin();
158    assertNotNull(findNamespace(admin, "default"));
159    assertNotNull(findNamespace(admin, "hbase"));
160
161    // Try (but fail) to delete namespaces via REST.
162    response = client.delete(defaultPath);
163    assertEquals(503, response.getCode());
164    response = client.delete(hbasePath);
165    assertEquals(503, response.getCode());
166
167    assertNotNull(findNamespace(admin, "default"));
168    assertNotNull(findNamespace(admin, "hbase"));
169  }
170
171  @Test
172  public void testGetNamespaceTablesAndCannotDeleteNamespace() throws IOException, JAXBException {
173    Admin admin = TEST_UTIL.getAdmin();
174    String nsName = "TestNamespacesInstanceResource5";
175    Response response;
176
177    // Create namespace via admin.
178    NamespaceDescriptor.Builder nsBuilder = NamespaceDescriptor.create(nsName);
179    NamespaceDescriptor nsd = nsBuilder.build();
180    nsd.setConfiguration("key1", "value1");
181    admin.createNamespace(nsd);
182
183    // Create two tables via admin.
184    TableName tn1 = TableName.valueOf(nsName + ":table1");
185    TableDescriptorBuilder tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(tn1);
186    ColumnFamilyDescriptor columnFamilyDescriptor =
187      ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes("cf1")).build();
188    tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
189    admin.createTable(tableDescriptorBuilder.build());
190    TableName tn2 = TableName.valueOf(nsName + ":table2");
191    tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(tn2);
192    tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
193    admin.createTable(tableDescriptorBuilder.build());
194
195    Map<String, String> nsProperties = new HashMap<>();
196    nsProperties.put("key1", "value1");
197    List<String> nsTables = Arrays.asList("table1", "table2");
198
199    // Check get namespace properties as XML, JSON and Protobuf.
200    String namespacePath = "/namespaces/" + nsName;
201    response = client.get(namespacePath);
202    assertEquals(200, response.getCode());
203
204    response = client.get(namespacePath, Constants.MIMETYPE_XML);
205    assertEquals(200, response.getCode());
206    NamespacesInstanceModel model = fromXML(response.getBody(), NamespacesInstanceModel.class);
207    checkNamespaceProperties(model.getProperties(), nsProperties);
208
209    response = client.get(namespacePath, Constants.MIMETYPE_JSON);
210    assertEquals(200, response.getCode());
211    model = jsonMapper.readValue(response.getBody(), NamespacesInstanceModel.class);
212    checkNamespaceProperties(model.getProperties(), nsProperties);
213
214    response = client.get(namespacePath, Constants.MIMETYPE_PROTOBUF);
215    assertEquals(200, response.getCode());
216    model.getObjectFromMessage(response.getBody());
217    checkNamespaceProperties(model.getProperties(), nsProperties);
218
219    // Check get namespace tables as XML, JSON and Protobuf.
220    namespacePath = "/namespaces/" + nsName + "/tables";
221    response = client.get(namespacePath);
222    assertEquals(200, response.getCode());
223
224    response = client.get(namespacePath, Constants.MIMETYPE_XML);
225    assertEquals(200, response.getCode());
226    TableListModel tablemodel = fromXML(response.getBody(), TableListModel.class);
227    checkNamespaceTables(tablemodel.getTables(), nsTables);
228
229    response = client.get(namespacePath, Constants.MIMETYPE_JSON);
230    assertEquals(200, response.getCode());
231    tablemodel = jsonMapper.readValue(response.getBody(), TableListModel.class);
232    checkNamespaceTables(tablemodel.getTables(), nsTables);
233
234    response = client.get(namespacePath, Constants.MIMETYPE_PROTOBUF);
235    assertEquals(200, response.getCode());
236    tablemodel.setTables(new ArrayList<>());
237    tablemodel.getObjectFromMessage(response.getBody());
238    checkNamespaceTables(tablemodel.getTables(), nsTables);
239
240    // Check cannot delete namespace via REST because it contains tables.
241    response = client.delete(namespacePath);
242    namespacePath = "/namespaces/" + nsName;
243    assertEquals(503, response.getCode());
244  }
245
246  @Test
247  public void testInvalidNamespacePostsAndPuts() throws IOException, JAXBException {
248    String namespacePath1 = "/namespaces/" + NAMESPACE1;
249    String namespacePath2 = "/namespaces/" + NAMESPACE2;
250    String namespacePath3 = "/namespaces/" + NAMESPACE3;
251    NamespacesInstanceModel model1;
252    NamespacesInstanceModel model2;
253    NamespacesInstanceModel model3;
254    Response response;
255
256    // Check that namespaces don't exist via non-REST call.
257    Admin admin = TEST_UTIL.getAdmin();
258    assertNull(findNamespace(admin, NAMESPACE1));
259    assertNull(findNamespace(admin, NAMESPACE2));
260    assertNull(findNamespace(admin, NAMESPACE3));
261
262    model1 = testNamespacesInstanceModel.buildTestModel(NAMESPACE1, NAMESPACE1_PROPS);
263    testNamespacesInstanceModel.checkModel(model1, NAMESPACE1, NAMESPACE1_PROPS);
264    model2 = testNamespacesInstanceModel.buildTestModel(NAMESPACE2, NAMESPACE2_PROPS);
265    testNamespacesInstanceModel.checkModel(model2, NAMESPACE2, NAMESPACE2_PROPS);
266    model3 = testNamespacesInstanceModel.buildTestModel(NAMESPACE3, NAMESPACE3_PROPS);
267    testNamespacesInstanceModel.checkModel(model3, NAMESPACE3, NAMESPACE3_PROPS);
268
269    // Try REST post and puts with invalid content.
270    response = client.post(namespacePath1, Constants.MIMETYPE_JSON, toXML(model1));
271    assertEquals(500, response.getCode());
272    String jsonString = jsonMapper.writeValueAsString(model2);
273    response = client.put(namespacePath2, Constants.MIMETYPE_XML, Bytes.toBytes(jsonString));
274    assertEquals(400, response.getCode());
275    response = client.post(namespacePath3, Constants.MIMETYPE_PROTOBUF, toXML(model3));
276    assertEquals(500, response.getCode());
277
278    NamespaceDescriptor nd1 = findNamespace(admin, NAMESPACE1);
279    NamespaceDescriptor nd2 = findNamespace(admin, NAMESPACE2);
280    NamespaceDescriptor nd3 = findNamespace(admin, NAMESPACE3);
281    assertNull(nd1);
282    assertNull(nd2);
283    assertNull(nd3);
284  }
285
286  @Test
287  public void testNamespaceCreateAndDeleteXMLAndJSON() throws IOException, JAXBException {
288    String namespacePath1 = "/namespaces/" + NAMESPACE1;
289    String namespacePath2 = "/namespaces/" + NAMESPACE2;
290    NamespacesInstanceModel model1;
291    NamespacesInstanceModel model2;
292    Response response;
293
294    // Check that namespaces don't exist via non-REST call.
295    Admin admin = TEST_UTIL.getAdmin();
296    assertNull(findNamespace(admin, NAMESPACE1));
297    assertNull(findNamespace(admin, NAMESPACE2));
298
299    model1 = testNamespacesInstanceModel.buildTestModel(NAMESPACE1, NAMESPACE1_PROPS);
300    testNamespacesInstanceModel.checkModel(model1, NAMESPACE1, NAMESPACE1_PROPS);
301    model2 = testNamespacesInstanceModel.buildTestModel(NAMESPACE2, NAMESPACE2_PROPS);
302    testNamespacesInstanceModel.checkModel(model2, NAMESPACE2, NAMESPACE2_PROPS);
303
304    // Test cannot PUT (alter) non-existent namespace.
305    response = client.put(namespacePath1, Constants.MIMETYPE_XML, toXML(model1));
306    assertEquals(403, response.getCode());
307    String jsonString = jsonMapper.writeValueAsString(model2);
308    response = client.put(namespacePath2, Constants.MIMETYPE_JSON, Bytes.toBytes(jsonString));
309    assertEquals(403, response.getCode());
310
311    // Test cannot create tables when in read only mode.
312    conf.set("hbase.rest.readonly", "true");
313    response = client.post(namespacePath1, Constants.MIMETYPE_XML, toXML(model1));
314    assertEquals(403, response.getCode());
315    jsonString = jsonMapper.writeValueAsString(model2);
316    response = client.post(namespacePath2, Constants.MIMETYPE_JSON, Bytes.toBytes(jsonString));
317    assertEquals(403, response.getCode());
318    NamespaceDescriptor nd1 = findNamespace(admin, NAMESPACE1);
319    NamespaceDescriptor nd2 = findNamespace(admin, NAMESPACE2);
320    assertNull(nd1);
321    assertNull(nd2);
322    conf.set("hbase.rest.readonly", "false");
323
324    // Create namespace via XML and JSON.
325    response = client.post(namespacePath1, Constants.MIMETYPE_XML, toXML(model1));
326    assertEquals(201, response.getCode());
327    jsonString = jsonMapper.writeValueAsString(model2);
328    response = client.post(namespacePath2, Constants.MIMETYPE_JSON, Bytes.toBytes(jsonString));
329    assertEquals(201, response.getCode());
330    // check passing null content-type with a payload returns 415
331    Header[] nullHeaders = null;
332    response = client.post(namespacePath1, nullHeaders, toXML(model1));
333    assertEquals(415, response.getCode());
334    response = client.post(namespacePath1, nullHeaders, Bytes.toBytes(jsonString));
335    assertEquals(415, response.getCode());
336
337    // Check that created namespaces correctly.
338    nd1 = findNamespace(admin, NAMESPACE1);
339    nd2 = findNamespace(admin, NAMESPACE2);
340    assertNotNull(nd1);
341    assertNotNull(nd2);
342    checkNamespaceProperties(nd1, NAMESPACE1_PROPS);
343    checkNamespaceProperties(nd1, NAMESPACE1_PROPS);
344
345    // Test cannot delete tables when in read only mode.
346    conf.set("hbase.rest.readonly", "true");
347    response = client.delete(namespacePath1);
348    assertEquals(403, response.getCode());
349    response = client.delete(namespacePath2);
350    assertEquals(403, response.getCode());
351    nd1 = findNamespace(admin, NAMESPACE1);
352    nd2 = findNamespace(admin, NAMESPACE2);
353    assertNotNull(nd1);
354    assertNotNull(nd2);
355    conf.set("hbase.rest.readonly", "false");
356
357    // Delete namespaces via XML and JSON.
358    response = client.delete(namespacePath1);
359    assertEquals(200, response.getCode());
360    response = client.delete(namespacePath2);
361    assertEquals(200, response.getCode());
362    nd1 = findNamespace(admin, NAMESPACE1);
363    nd2 = findNamespace(admin, NAMESPACE2);
364    assertNull(nd1);
365    assertNull(nd2);
366  }
367
368  @Test
369  public void testNamespaceCreateAndDeletePBAndNoBody() throws IOException {
370    String namespacePath3 = "/namespaces/" + NAMESPACE3;
371    String namespacePath4 = "/namespaces/" + NAMESPACE4;
372    NamespacesInstanceModel model3;
373    NamespacesInstanceModel model4;
374    Response response;
375
376    // Check that namespaces don't exist via non-REST call.
377    Admin admin = TEST_UTIL.getAdmin();
378    assertNull(findNamespace(admin, NAMESPACE3));
379    assertNull(findNamespace(admin, NAMESPACE4));
380
381    model3 = testNamespacesInstanceModel.buildTestModel(NAMESPACE3, NAMESPACE3_PROPS);
382    testNamespacesInstanceModel.checkModel(model3, NAMESPACE3, NAMESPACE3_PROPS);
383    model4 = testNamespacesInstanceModel.buildTestModel(NAMESPACE4, NAMESPACE4_PROPS);
384    testNamespacesInstanceModel.checkModel(model4, NAMESPACE4, NAMESPACE4_PROPS);
385
386    // Defines null headers for use in tests where no body content is provided, so that we set
387    // no content-type in the request
388    Header[] nullHeaders = null;
389
390    // Test cannot PUT (alter) non-existent namespace.
391    response = client.put(namespacePath3, nullHeaders, new byte[] {});
392    assertEquals(403, response.getCode());
393    response =
394      client.put(namespacePath4, Constants.MIMETYPE_PROTOBUF, model4.createProtobufOutput());
395    assertEquals(403, response.getCode());
396
397    // Test cannot create tables when in read only mode.
398    conf.set("hbase.rest.readonly", "true");
399    response = client.post(namespacePath3, nullHeaders, new byte[] {});
400    assertEquals(403, response.getCode());
401    response =
402      client.put(namespacePath4, Constants.MIMETYPE_PROTOBUF, model4.createProtobufOutput());
403    assertEquals(403, response.getCode());
404    NamespaceDescriptor nd3 = findNamespace(admin, NAMESPACE3);
405    NamespaceDescriptor nd4 = findNamespace(admin, NAMESPACE4);
406    assertNull(nd3);
407    assertNull(nd4);
408    conf.set("hbase.rest.readonly", "false");
409
410    // Create namespace with no body and binary content type.
411    response = client.post(namespacePath3, nullHeaders, new byte[] {});
412    assertEquals(201, response.getCode());
413    // Create namespace with protobuf content-type.
414    response =
415      client.post(namespacePath4, Constants.MIMETYPE_PROTOBUF, model4.createProtobufOutput());
416    assertEquals(201, response.getCode());
417    // check setting unsupported content-type returns 415
418    response = client.post(namespacePath3, Constants.MIMETYPE_BINARY, new byte[] {});
419    assertEquals(415, response.getCode());
420
421    // Check that created namespaces correctly.
422    nd3 = findNamespace(admin, NAMESPACE3);
423    nd4 = findNamespace(admin, NAMESPACE4);
424    assertNotNull(nd3);
425    assertNotNull(nd4);
426    checkNamespaceProperties(nd3, new HashMap<>());
427    checkNamespaceProperties(nd4, NAMESPACE4_PROPS);
428
429    // Check cannot post tables that already exist.
430    response = client.post(namespacePath3, nullHeaders, new byte[] {});
431    assertEquals(403, response.getCode());
432    response =
433      client.post(namespacePath4, Constants.MIMETYPE_PROTOBUF, model4.createProtobufOutput());
434    assertEquals(403, response.getCode());
435
436    // Check cannot post tables when in read only mode.
437    conf.set("hbase.rest.readonly", "true");
438    response = client.delete(namespacePath3);
439    assertEquals(403, response.getCode());
440    response = client.delete(namespacePath4);
441    assertEquals(403, response.getCode());
442    nd3 = findNamespace(admin, NAMESPACE3);
443    nd4 = findNamespace(admin, NAMESPACE4);
444    assertNotNull(nd3);
445    assertNotNull(nd4);
446    conf.set("hbase.rest.readonly", "false");
447
448    // Delete namespaces via XML and JSON.
449    response = client.delete(namespacePath3);
450    assertEquals(200, response.getCode());
451    response = client.delete(namespacePath4);
452    assertEquals(200, response.getCode());
453    nd3 = findNamespace(admin, NAMESPACE3);
454    nd4 = findNamespace(admin, NAMESPACE4);
455    assertNull(nd3);
456    assertNull(nd4);
457  }
458}