/*
* Copyright © 2014-2015 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package co.cask.cdap.internal.app.services.http.handlers;
import co.cask.cdap.AppForUnrecoverableResetTest;
import co.cask.cdap.AppWithDataset;
import co.cask.cdap.AppWithServices;
import co.cask.cdap.AppWithStreamSizeSchedule;
import co.cask.cdap.common.NotFoundException;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.namespace.AbstractNamespaceClient;
import co.cask.cdap.common.namespace.NamespacedLocationFactory;
import co.cask.cdap.data2.dataset2.DatasetFramework;
import co.cask.cdap.data2.transaction.stream.StreamAdmin;
import co.cask.cdap.gateway.handlers.NamespaceHttpHandler;
import co.cask.cdap.internal.app.services.http.AppFabricTestBase;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.NamespaceConfig;
import co.cask.cdap.proto.NamespaceMeta;
import co.cask.cdap.proto.ProgramType;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import org.apache.http.HttpResponse;
import org.apache.twill.filesystem.Location;
import org.junit.Assert;
import org.junit.Test;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Set;
/**
* Tests for {@link NamespaceHttpHandler}
*/
public class NamespaceHttpHandlerTest extends AppFabricTestBase {
private static final String EMPTY = "";
private static final String NAME_FIELD = "name";
private static final String DESCRIPTION_FIELD = "description";
private static final String CONFIG_FIELD = "config";
private static final String NAME = "test";
private static final Id.Namespace NAME_ID = Id.Namespace.from(NAME);
private static final String DESCRIPTION = "test description";
private static final String METADATA_VALID =
String.format("{\"%s\":\"%s\", \"%s\":\"%s\"}", NAME_FIELD, NAME, DESCRIPTION_FIELD, DESCRIPTION);
private static final String METADATA_MISSING_FIELDS = "{}";
private static final String METADATA_EMPTY_FIELDS = "{\"name\":\"\", \"description\":\"\"}";
private static final String METADATA_INVALID_JSON = "invalid";
private static final String INVALID_NAME = "!nv@l*d/";
private static final String OTHER_NAME = "test1";
private static final Gson GSON = new Gson();
private void assertResponseCode(int expected, HttpResponse response) {
Assert.assertEquals(expected, response.getStatusLine().getStatusCode());
}
private List<JsonObject> readListResponse(HttpResponse response) throws IOException {
Type typeToken = new TypeToken<List<JsonObject>>() { }.getType();
return readResponse(response, typeToken);
}
private JsonObject readGetResponse(HttpResponse response) throws IOException {
Type typeToken = new TypeToken<JsonObject>() { }.getType();
return readResponse(response, typeToken);
}
@Test
public void testNamespacesValidFlows() throws Exception {
// get initial namespace list
HttpResponse response = listAllNamespaces();
assertResponseCode(200, response);
List<JsonObject> namespaces = readListResponse(response);
int initialSize = namespaces.size();
// create and verify
response = createNamespace(METADATA_VALID, NAME);
assertResponseCode(200, response);
response = listAllNamespaces();
namespaces = readListResponse(response);
Assert.assertEquals(initialSize + 1, namespaces.size());
Assert.assertEquals(NAME, namespaces.get(0).get(NAME_FIELD).getAsString());
Assert.assertEquals(DESCRIPTION, namespaces.get(0).get(DESCRIPTION_FIELD).getAsString());
// cleanup
response = deleteNamespace(NAME);
assertResponseCode(200, response);
response = listAllNamespaces();
namespaces = readListResponse(response);
Assert.assertEquals(initialSize, namespaces.size());
}
@Test
public void testCreateDuplicate() throws Exception {
// prepare - create namespace
HttpResponse response = createNamespace(METADATA_VALID, NAME);
assertResponseCode(200, response);
response = getNamespace(NAME);
JsonObject namespace = readGetResponse(response);
Assert.assertNotNull(namespace);
Assert.assertEquals(NAME, namespace.get(NAME_FIELD).getAsString());
Assert.assertEquals(DESCRIPTION, namespace.get(DESCRIPTION_FIELD).getAsString());
// create again with the same name
response = createNamespace(METADATA_EMPTY_FIELDS, NAME);
// create is idempotent, so response code is 200, but no updates should happen
assertResponseCode(200, response);
// check that no updates happened
response = getNamespace(NAME);
namespace = readGetResponse(response);
Assert.assertNotNull(namespace);
Assert.assertEquals(NAME, namespace.get(NAME_FIELD).getAsString());
Assert.assertEquals(DESCRIPTION, namespace.get(DESCRIPTION_FIELD).getAsString());
// cleanup
response = deleteNamespace(NAME);
assertResponseCode(200, response);
}
@Test
public void testCreateWithConfig() throws Exception {
// prepare - create namespace with config in its properties
String propertiesString = String.format("{\"%s\":\"%s\", \"%s\":\"%s\", \"%s\":%s}",
NAME_FIELD, NAME, DESCRIPTION_FIELD, DESCRIPTION,
CONFIG_FIELD, "{\"scheduler.queue.name\":\"testSchedulerQueueName\"}");
HttpResponse response = createNamespace(propertiesString, NAME);
assertResponseCode(200, response);
response = getNamespace(NAME);
JsonObject namespace = readGetResponse(response);
Assert.assertNotNull(namespace);
Assert.assertEquals(NAME, namespace.get(NAME_FIELD).getAsString());
Assert.assertEquals(DESCRIPTION, namespace.get(DESCRIPTION_FIELD).getAsString());
Assert.assertEquals("testSchedulerQueueName",
namespace.get(CONFIG_FIELD).getAsJsonObject().get("scheduler.queue.name").getAsString());
response = deleteNamespace(NAME);
assertResponseCode(200, response);
}
@Test
public void testInvalidReservedId() throws Exception {
HttpResponse response = createNamespace(METADATA_VALID, INVALID_NAME);
assertResponseCode(400, response);
// '-' is not allowed anymore
response = createNamespace(METADATA_VALID, "my-namespace");
assertResponseCode(400, response);
// 'default' and 'system' are reserved namespaces
response = createNamespace(METADATA_VALID, Id.Namespace.DEFAULT.getId());
assertResponseCode(400, response);
response = createNamespace(METADATA_VALID, Id.Namespace.SYSTEM.getId());
assertResponseCode(400, response);
// we allow deleting the contents in default namespace. However, the namespace itself should never be deleted
deploy(AppWithDataset.class);
response = deleteNamespace(Id.Namespace.DEFAULT.getId());
assertResponseCode(200, response);
response = getNamespace(Id.Namespace.DEFAULT.getId());
Assert.assertEquals(0, getAppList(Id.Namespace.DEFAULT.getId()).size());
assertResponseCode(200, response);
// there is no system namespace
response = deleteNamespace(Id.Namespace.SYSTEM.getId());
assertResponseCode(404, response);
}
@Test
public void testCreateInvalidJson() throws Exception {
// invalid json should return 400
HttpResponse response = createNamespace(METADATA_INVALID_JSON, NAME);
assertResponseCode(400, response);
// verify
response = getNamespace(NAME);
assertResponseCode(404, response);
}
@Test
public void testCreateMissingOrEmptyFields() throws Exception {
// create with no metadata
HttpResponse response = createNamespace(NAME);
assertResponseCode(200, response);
// verify
response = getNamespace(NAME);
JsonObject namespace = readGetResponse(response);
Assert.assertNotNull(namespace);
Assert.assertEquals(NAME, namespace.get(NAME_FIELD).getAsString());
Assert.assertEquals(EMPTY, namespace.get(DESCRIPTION_FIELD).getAsString());
// cleanup
response = deleteNamespace(NAME);
assertResponseCode(200, response);
// create with missing fields
response = createNamespace(METADATA_MISSING_FIELDS, NAME);
assertResponseCode(200, response);
// verify
response = getNamespace(NAME);
namespace = readGetResponse(response);
Assert.assertNotNull(namespace);
Assert.assertEquals(NAME, namespace.get(NAME_FIELD).getAsString());
Assert.assertEquals(EMPTY, namespace.get(DESCRIPTION_FIELD).getAsString());
// cleanup
response = deleteNamespace(NAME);
assertResponseCode(200, response);
// create with empty fields
response = createNamespace(METADATA_EMPTY_FIELDS, NAME);
assertResponseCode(200, response);
// verify
response = getNamespace(NAME);
namespace = readGetResponse(response);
Assert.assertNotNull(namespace);
Assert.assertEquals(NAME, namespace.get(NAME_FIELD).getAsString());
Assert.assertEquals(EMPTY, namespace.get(DESCRIPTION_FIELD).getAsString());
// cleanup
response = deleteNamespace(NAME);
assertResponseCode(200, response);
}
@Test
public void testDeleteAll() throws Exception {
CConfiguration cConf = getInjector().getInstance(CConfiguration.class);
// test deleting non-existent namespace
assertResponseCode(404, deleteNamespace("doesnotexist"));
assertResponseCode(200, createNamespace(NAME));
assertResponseCode(200, getNamespace(NAME));
assertResponseCode(200, createNamespace(OTHER_NAME));
assertResponseCode(200, getNamespace(OTHER_NAME));
NamespacedLocationFactory namespacedLocationFactory = getInjector().getInstance(NamespacedLocationFactory.class);
Location nsLocation = namespacedLocationFactory.get(Id.Namespace.from(NAME));
Assert.assertTrue(nsLocation.exists());
DatasetFramework dsFramework = getInjector().getInstance(DatasetFramework.class);
StreamAdmin streamAdmin = getInjector().getInstance(StreamAdmin.class);
deploy(AppWithServices.class, Constants.Gateway.API_VERSION_3_TOKEN, NAME);
deploy(AppWithDataset.class, Constants.Gateway.API_VERSION_3_TOKEN, NAME);
deploy(AppWithStreamSizeSchedule.class, Constants.Gateway.API_VERSION_3_TOKEN, OTHER_NAME);
deploy(AppForUnrecoverableResetTest.class, Constants.Gateway.API_VERSION_3_TOKEN, OTHER_NAME);
Id.DatasetInstance myDataset = Id.DatasetInstance.from(NAME, "myds");
Id.Stream myStream = Id.Stream.from(OTHER_NAME, "stream");
Assert.assertTrue(dsFramework.hasInstance(myDataset));
Assert.assertTrue(streamAdmin.exists(myStream));
Id.Program program = Id.Program.from(NAME_ID, "AppWithServices", ProgramType.SERVICE, "NoOpService");
startProgram(program);
boolean resetEnabled = cConf.getBoolean(Constants.Dangerous.UNRECOVERABLE_RESET);
cConf.setBoolean(Constants.Dangerous.UNRECOVERABLE_RESET, false);
// because unrecoverable reset is disabled
assertResponseCode(403, deleteNamespace(NAME));
cConf.setBoolean(Constants.Dangerous.UNRECOVERABLE_RESET, resetEnabled);
// because service is running
assertResponseCode(409, deleteNamespace(NAME));
Assert.assertTrue(nsLocation.exists());
stopProgram(program);
// delete should work now
assertResponseCode(200, deleteNamespace(NAME));
Assert.assertFalse(nsLocation.exists());
Assert.assertFalse(dsFramework.hasInstance(myDataset));
Assert.assertTrue(streamAdmin.exists(myStream));
assertResponseCode(200, deleteNamespace(OTHER_NAME));
Assert.assertFalse(streamAdmin.exists(myStream));
// Create the namespace again and deploy the application containing schedules.
// Application deployment should succeed.
assertResponseCode(200, createNamespace(OTHER_NAME));
HttpResponse response = deploy(AppForUnrecoverableResetTest.class, Constants.Gateway.API_VERSION_3_TOKEN,
OTHER_NAME);
Assert.assertEquals(200, response.getStatusLine().getStatusCode());
assertResponseCode(200, deleteNamespace(OTHER_NAME));
}
@Test
public void testDeleteDatasetsOnly() throws Exception {
CConfiguration cConf = getInjector().getInstance(CConfiguration.class);
// test deleting non-existent namespace
assertResponseCode(200, createNamespace(NAME));
assertResponseCode(200, getNamespace(NAME));
NamespacedLocationFactory namespacedLocationFactory = getInjector().getInstance(NamespacedLocationFactory.class);
Location nsLocation = namespacedLocationFactory.get(Id.Namespace.from(NAME));
Assert.assertTrue(nsLocation.exists());
DatasetFramework dsFramework = getInjector().getInstance(DatasetFramework.class);
deploy(AppWithServices.class, Constants.Gateway.API_VERSION_3_TOKEN, NAME);
deploy(AppWithDataset.class, Constants.Gateway.API_VERSION_3_TOKEN, NAME);
Id.DatasetInstance myDataset = Id.DatasetInstance.from(NAME, "myds");
Assert.assertTrue(dsFramework.hasInstance(myDataset));
Id.Program program = Id.Program.from(NAME_ID, "AppWithServices", ProgramType.SERVICE, "NoOpService");
startProgram(program);
boolean resetEnabled = cConf.getBoolean(Constants.Dangerous.UNRECOVERABLE_RESET);
cConf.setBoolean(Constants.Dangerous.UNRECOVERABLE_RESET, false);
// because reset is not enabled
assertResponseCode(403, deleteNamespaceData(NAME));
Assert.assertTrue(nsLocation.exists());
cConf.setBoolean(Constants.Dangerous.UNRECOVERABLE_RESET, resetEnabled);
// because service is running
assertResponseCode(409, deleteNamespaceData(NAME));
Assert.assertTrue(nsLocation.exists());
stopProgram(program);
assertResponseCode(200, deleteNamespaceData(NAME));
Assert.assertTrue(nsLocation.exists());
Assert.assertTrue(getAppList(NAME).size() == 2);
Assert.assertTrue(getAppDetails(NAME, "AppWithServices").get("name").getAsString().equals("AppWithServices"));
Assert.assertTrue(getAppDetails(NAME, AppWithDataset.class.getSimpleName()).get("name").getAsString()
.equals(AppWithDataset.class.getSimpleName()));
assertResponseCode(200, getNamespace(NAME));
Assert.assertFalse(dsFramework.hasInstance(myDataset));
assertResponseCode(200, deleteNamespace(NAME));
assertResponseCode(404, getNamespace(NAME));
}
@Test
public void testNamespaceClient() throws Exception {
// tests the NamespaceClient's ability to interact with Namespace service/handlers.
AbstractNamespaceClient namespaceClient = getInjector().getInstance(AbstractNamespaceClient.class);
// test setup creates two namespaces in @BeforeClass, apart from the default namespace which always exists.
List<NamespaceMeta> namespaces = namespaceClient.list();
Assert.assertEquals(3, namespaces.size());
Set<NamespaceMeta> expectedNamespaces = ImmutableSet.of(NamespaceMeta.DEFAULT, TEST_NAMESPACE_META1,
TEST_NAMESPACE_META2);
Assert.assertEquals(expectedNamespaces, Sets.newHashSet(namespaces));
NamespaceMeta namespaceMeta = namespaceClient.get(Id.Namespace.from(TEST_NAMESPACE1));
Assert.assertEquals(TEST_NAMESPACE_META1, namespaceMeta);
try {
namespaceClient.get(Id.Namespace.from("nonExistentNamespace"));
Assert.fail("Did not expect namespace 'nonExistentNamespace' to exist.");
} catch (NotFoundException expected) {
// expected
}
// test create and get
Id.Namespace fooNamespace = Id.Namespace.from("fooNamespace");
NamespaceMeta toCreate = new NamespaceMeta.Builder().setName(fooNamespace).build();
namespaceClient.create(toCreate);
NamespaceMeta receivedMeta = namespaceClient.get(fooNamespace);
Assert.assertNotNull(receivedMeta);
Assert.assertEquals(toCreate, receivedMeta);
namespaceClient.delete(fooNamespace);
try {
namespaceClient.get(fooNamespace);
Assert.fail("Did not expect namespace '" + fooNamespace + "' to exist after deleting it.");
} catch (NotFoundException expected) {
// expected
}
}
@Test
public void testProperties() throws Exception {
// create with no metadata
HttpResponse response = createNamespace(NAME);
assertResponseCode(200, response);
// verify
response = getNamespace(NAME);
JsonObject namespace = readGetResponse(response);
Assert.assertNotNull(namespace);
Assert.assertEquals(NAME, namespace.get(NAME_FIELD).getAsString());
Assert.assertEquals(EMPTY, namespace.get(DESCRIPTION_FIELD).getAsString());
// Update scheduler queue name.
String nonexistentName = NAME + "nonexistent";
NamespaceMeta meta = new NamespaceMeta.Builder().setName(nonexistentName).setSchedulerQueueName("prod").build();
setProperties(NAME, meta);
// assert that the name in the metadata is ignored (the name from the url should be used, instead
HttpResponse nonexistentGet = getNamespace(nonexistentName);
Assert.assertEquals(404, nonexistentGet.getStatusLine().getStatusCode());
response = getNamespace(NAME);
namespace = readGetResponse(response);
Assert.assertNotNull(namespace);
NamespaceConfig config = GSON.fromJson(namespace.get(CONFIG_FIELD).getAsJsonObject(),
NamespaceConfig.class);
Assert.assertEquals("prod", config.getSchedulerQueueName());
Assert.assertEquals(NAME, namespace.get(NAME_FIELD).getAsString());
Assert.assertEquals(EMPTY, namespace.get(DESCRIPTION_FIELD).getAsString());
// Update description
meta = new NamespaceMeta.Builder().setName(NAME).setDescription("new fancy description").build();
setProperties(NAME, meta);
response = getNamespace(NAME);
namespace = readGetResponse(response);
Assert.assertNotNull(namespace);
//verify that the description has changed
Assert.assertEquals("new fancy description", namespace.get(DESCRIPTION_FIELD).getAsString());
Assert.assertEquals(NAME, namespace.get(NAME_FIELD).getAsString());
// verify other properties set earlier has not changed.
config = GSON.fromJson(namespace.get(CONFIG_FIELD).getAsJsonObject(), NamespaceConfig.class);
Assert.assertEquals("prod", config.getSchedulerQueueName());
// cleanup
response = deleteNamespace(NAME);
Assert.assertEquals(200, response.getStatusLine().getStatusCode());
}
}