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