/*
* Copyright © 2015-2016 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.namespace;
import co.cask.cdap.api.dataset.DatasetManagementException;
import co.cask.cdap.api.metrics.MetricDeleteQuery;
import co.cask.cdap.api.metrics.MetricStore;
import co.cask.cdap.app.runtime.ProgramRuntimeService;
import co.cask.cdap.app.store.Store;
import co.cask.cdap.common.NamespaceAlreadyExistsException;
import co.cask.cdap.common.NamespaceCannotBeCreatedException;
import co.cask.cdap.common.NamespaceCannotBeDeletedException;
import co.cask.cdap.common.NamespaceNotFoundException;
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.NamespaceAdmin;
import co.cask.cdap.config.DashboardStore;
import co.cask.cdap.config.PreferencesStore;
import co.cask.cdap.data2.dataset2.DatasetFramework;
import co.cask.cdap.data2.transaction.queue.QueueAdmin;
import co.cask.cdap.data2.transaction.stream.StreamAdmin;
import co.cask.cdap.internal.app.runtime.artifact.ArtifactRepository;
import co.cask.cdap.internal.app.runtime.schedule.Scheduler;
import co.cask.cdap.internal.app.services.ApplicationLifecycleService;
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 co.cask.cdap.proto.id.InstanceId;
import co.cask.cdap.proto.id.NamespaceId;
import co.cask.cdap.proto.security.Action;
import co.cask.cdap.proto.security.Principal;
import co.cask.cdap.security.authorization.AuthorizerInstantiatorService;
import co.cask.cdap.security.spi.authentication.SecurityRequestContext;
import co.cask.cdap.store.NamespaceStore;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Admin for managing namespaces.
*/
public final class DefaultNamespaceAdmin implements NamespaceAdmin {
private static final Logger LOG = LoggerFactory.getLogger(DefaultNamespaceAdmin.class);
private final Store store;
private final NamespaceStore nsStore;
private final PreferencesStore preferencesStore;
private final DashboardStore dashboardStore;
private final DatasetFramework dsFramework;
private final ProgramRuntimeService runtimeService;
private final QueueAdmin queueAdmin;
private final StreamAdmin streamAdmin;
private final MetricStore metricStore;
private final Scheduler scheduler;
private final ApplicationLifecycleService applicationLifecycleService;
private final ArtifactRepository artifactRepository;
private final AuthorizerInstantiatorService authorizerInstantiatorService;
private final InstanceId instanceId;
private final Pattern namespacePattern = Pattern.compile("[a-zA-Z0-9_]+");
@Inject
DefaultNamespaceAdmin(Store store, NamespaceStore nsStore, PreferencesStore preferencesStore,
DashboardStore dashboardStore, DatasetFramework dsFramework,
ProgramRuntimeService runtimeService, QueueAdmin queueAdmin, StreamAdmin streamAdmin,
MetricStore metricStore, Scheduler scheduler,
ApplicationLifecycleService applicationLifecycleService,
ArtifactRepository artifactRepository,
AuthorizerInstantiatorService authorizerInstantiatorService,
CConfiguration cConf) {
this.queueAdmin = queueAdmin;
this.streamAdmin = streamAdmin;
this.store = store;
this.nsStore = nsStore;
this.preferencesStore = preferencesStore;
this.dashboardStore = dashboardStore;
this.dsFramework = dsFramework;
this.runtimeService = runtimeService;
this.scheduler = scheduler;
this.metricStore = metricStore;
this.applicationLifecycleService = applicationLifecycleService;
this.artifactRepository = artifactRepository;
this.authorizerInstantiatorService = authorizerInstantiatorService;
this.instanceId = createInstanceId(cConf);
}
/**
* Lists all namespaces
*
* @return a list of {@link NamespaceMeta} for all namespaces
*/
@Override
public List<NamespaceMeta> list() throws Exception {
return nsStore.list();
}
/**
* Gets details of a namespace
*
* @param namespaceId the {@link Id.Namespace} of the requested namespace
* @return the {@link NamespaceMeta} of the requested namespace
* @throws NamespaceNotFoundException if the requested namespace is not found
*/
@Override
public NamespaceMeta get(Id.Namespace namespaceId) throws Exception {
NamespaceMeta ns = nsStore.get(namespaceId);
if (ns == null) {
throw new NamespaceNotFoundException(namespaceId);
}
return ns;
}
/**
* Checks if the specified namespace exists
*
* @param namespaceId the {@link Id.Namespace} to check for existence
* @return true, if the specifed namespace exists, false otherwise
*/
@Override
public boolean exists(Id.Namespace namespaceId) throws Exception {
try {
get(namespaceId);
} catch (NotFoundException e) {
return false;
}
return true;
}
/**
* Creates a new namespace
*
* @param metadata the {@link NamespaceMeta} for the new namespace to be created
* @throws NamespaceAlreadyExistsException if the specified namespace already exists
*/
@Override
public synchronized void create(NamespaceMeta metadata) throws Exception {
// TODO: CDAP-1427 - This should be transactional, but we don't support transactions on files yet
Preconditions.checkArgument(metadata != null, "Namespace metadata should not be null.");
NamespaceId namespace = new NamespaceId(metadata.getName());
if (exists(namespace.toId())) {
throw new NamespaceAlreadyExistsException(namespace.toId());
}
// Namespace can be created. Check if the user is authorized now.
Principal principal = SecurityRequestContext.toPrincipal();
// Skip authorization enforcement for the system user and the default namespace, so the DefaultNamespaceEnsurer
// thread can successfully create the default namespace
if (!(Principal.SYSTEM.equals(principal) && NamespaceId.DEFAULT.equals(namespace))) {
authorizerInstantiatorService.get().enforce(instanceId, principal, Action.ADMIN);
}
try {
dsFramework.createNamespace(namespace.toId());
} catch (DatasetManagementException e) {
throw new NamespaceCannotBeCreatedException(namespace.toId(), e);
}
nsStore.create(metadata);
// Skip authorization grants for the system user
if (!(Principal.SYSTEM.equals(principal) && NamespaceId.DEFAULT.equals(namespace))) {
authorizerInstantiatorService.get().grant(namespace, principal, ImmutableSet.of(Action.ALL));
}
}
/**
* Deletes the specified namespace
*
* @param namespaceId the {@link Id.Namespace} of the specified namespace
* @throws NamespaceCannotBeDeletedException if the specified namespace cannot be deleted
* @throws NamespaceNotFoundException if the specified namespace does not exist
*/
@Override
public synchronized void delete(final Id.Namespace namespaceId) throws Exception {
NamespaceId namespace = namespaceId.toEntityId();
// TODO: CDAP-870, CDAP-1427: Delete should be in a single transaction.
if (!exists(namespaceId)) {
throw new NamespaceNotFoundException(namespaceId);
}
if (checkProgramsRunning(namespaceId.toEntityId())) {
throw new NamespaceCannotBeDeletedException(namespaceId,
String.format("Some programs are currently running in namespace " +
"'%s', please stop them before deleting namespace",
namespaceId));
}
// Namespace can be deleted. Revoke all privileges first
authorizerInstantiatorService.get().enforce(namespace, SecurityRequestContext.toPrincipal(), Action.ADMIN);
authorizerInstantiatorService.get().revoke(namespace);
LOG.info("Deleting namespace '{}'.", namespaceId);
try {
// Delete Preferences associated with this namespace
preferencesStore.deleteProperties(namespaceId.getId());
// Delete all dashboards associated with this namespace
dashboardStore.delete(namespaceId.getId());
// Delete all applications
applicationLifecycleService.removeAll(namespaceId);
// Delete all the schedules
scheduler.deleteAllSchedules(namespaceId);
// Delete datasets and modules
dsFramework.deleteAllInstances(namespaceId);
dsFramework.deleteAllModules(namespaceId);
// Delete queues and streams data
queueAdmin.dropAllInNamespace(namespaceId);
streamAdmin.dropAllInNamespace(namespaceId);
// Delete all meta data
store.removeAll(namespaceId);
deleteMetrics(namespaceId.toEntityId());
// delete all artifacts in the namespace
artifactRepository.clear(namespaceId.toEntityId());
// Delete the namespace itself, only if it is a non-default namespace. This is because we do not allow users to
// create default namespace, and hence deleting it may cause undeterministic behavior.
// Another reason for not deleting the default namespace is that we do not want to call a delete on the default
// namespace in the storage provider (Hive, HBase, etc), since we re-use their default namespace.
if (!Id.Namespace.DEFAULT.equals(namespaceId)) {
// Finally delete namespace from MDS
nsStore.delete(namespaceId);
// Delete namespace in storage providers
dsFramework.deleteNamespace(namespaceId);
}
} catch (Exception e) {
LOG.warn("Error while deleting namespace {}", namespaceId, e);
throw new NamespaceCannotBeDeletedException(namespaceId, e);
}
LOG.info("All data for namespace '{}' deleted.", namespaceId);
}
private void deleteMetrics(NamespaceId namespaceId) throws Exception {
long endTs = System.currentTimeMillis() / 1000;
Map<String, String> tags = Maps.newHashMap();
tags.put(Constants.Metrics.Tag.NAMESPACE, namespaceId.getNamespace());
MetricDeleteQuery deleteQuery = new MetricDeleteQuery(0, endTs, tags);
metricStore.delete(deleteQuery);
}
@Override
public synchronized void deleteDatasets(Id.Namespace namespaceId) throws Exception {
// TODO: CDAP-870, CDAP-1427: Delete should be in a single transaction.
if (!exists(namespaceId)) {
throw new NamespaceNotFoundException(namespaceId);
}
if (checkProgramsRunning(namespaceId.toEntityId())) {
throw new NamespaceCannotBeDeletedException(namespaceId,
String.format("Some programs are currently running in namespace " +
"'%s', please stop them before deleting datasets " +
"in the namespace.",
namespaceId));
}
// Namespace data can be deleted. Revoke all privileges first
authorizerInstantiatorService.get().enforce(namespaceId.toEntityId(), SecurityRequestContext.toPrincipal(),
Action.ADMIN);
try {
dsFramework.deleteAllInstances(namespaceId);
} catch (DatasetManagementException | IOException e) {
LOG.warn("Error while deleting datasets in namespace {}", namespaceId, e);
throw new NamespaceCannotBeDeletedException(namespaceId, e);
}
LOG.debug("Deleted datasets in namespace '{}'.", namespaceId);
}
@Override
public synchronized void updateProperties(Id.Namespace namespaceId, NamespaceMeta namespaceMeta) throws Exception {
if (!exists(namespaceId)) {
throw new NamespaceNotFoundException(namespaceId);
}
authorizerInstantiatorService.get().enforce(namespaceId.toEntityId(), SecurityRequestContext.toPrincipal(),
Action.ADMIN);
NamespaceMeta metadata = nsStore.get(namespaceId);
NamespaceMeta.Builder builder = new NamespaceMeta.Builder(metadata);
if (namespaceMeta.getDescription() != null) {
builder.setDescription(namespaceMeta.getDescription());
}
NamespaceConfig config = namespaceMeta.getConfig();
if (config != null && !Strings.isNullOrEmpty(config.getSchedulerQueueName())) {
builder.setSchedulerQueueName(config.getSchedulerQueueName());
}
nsStore.update(builder.build());
}
private boolean checkProgramsRunning(final NamespaceId namespaceId) {
return runtimeService.checkAnyRunning(new Predicate<Id.Program>() {
@Override
public boolean apply(Id.Program program) {
return program.getNamespaceId().equals(namespaceId.getNamespace());
}
}, ProgramType.values());
}
private InstanceId createInstanceId(CConfiguration cConf) {
String instanceName = cConf.get(Constants.INSTANCE_NAME);
Preconditions.checkArgument(namespacePattern.matcher(instanceName).matches(),
"CDAP instance name specified by '%s' in cdap-site.xml should be alphanumeric " +
"(underscores allowed). Its current invalid value is '%s'",
Constants.INSTANCE_NAME, instanceName);
return new InstanceId(instanceName);
}
}