package com.thinkbiganalytics.feedmgr.service;
/*-
* #%L
* thinkbig-feed-manager-controller
* %%
* Copyright (C) 2017 ThinkBig Analytics
* %%
* 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.
* #L%
*/
import com.thinkbiganalytics.datalake.authorization.service.HadoopAuthorizationService;
import com.thinkbiganalytics.feedmgr.InvalidOperationException;
import com.thinkbiganalytics.feedmgr.rest.model.FeedCategory;
import com.thinkbiganalytics.feedmgr.rest.model.FeedMetadata;
import com.thinkbiganalytics.feedmgr.rest.model.FeedSummary;
import com.thinkbiganalytics.feedmgr.rest.model.NifiFeed;
import com.thinkbiganalytics.feedmgr.rest.model.RegisteredTemplate;
import com.thinkbiganalytics.feedmgr.rest.model.UIFeed;
import com.thinkbiganalytics.feedmgr.rest.model.UserFieldCollection;
import com.thinkbiganalytics.feedmgr.rest.model.UserProperty;
import com.thinkbiganalytics.feedmgr.security.FeedServicesAccessControl;
import com.thinkbiganalytics.feedmgr.service.category.FeedManagerCategoryService;
import com.thinkbiganalytics.feedmgr.service.feed.FeedManagerFeedService;
import com.thinkbiganalytics.feedmgr.service.feed.FeedModelTransform;
import com.thinkbiganalytics.feedmgr.service.template.FeedManagerTemplateService;
import com.thinkbiganalytics.metadata.api.MetadataAccess;
import com.thinkbiganalytics.metadata.api.event.MetadataEventListener;
import com.thinkbiganalytics.metadata.api.event.MetadataEventService;
import com.thinkbiganalytics.metadata.api.event.feed.CleanupTriggerEvent;
import com.thinkbiganalytics.metadata.api.event.feed.FeedOperationStatusEvent;
import com.thinkbiganalytics.metadata.api.feed.Feed;
import com.thinkbiganalytics.metadata.api.op.FeedOperation;
import com.thinkbiganalytics.nifi.rest.client.LegacyNifiRestClient;
import com.thinkbiganalytics.nifi.rest.client.NiFiComponentState;
import com.thinkbiganalytics.nifi.rest.client.NiFiRestClient;
import com.thinkbiganalytics.nifi.rest.model.NifiProperty;
import com.thinkbiganalytics.nifi.rest.support.NifiProcessUtil;
import com.thinkbiganalytics.security.AccessController;
import com.thinkbiganalytics.security.action.Action;
import org.apache.nifi.web.api.dto.ConnectionDTO;
import org.apache.nifi.web.api.dto.ProcessGroupDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.ws.rs.NotFoundException;
/**
* Provides access to category, feed, and template metadata stored in the metadata store.
*/
public class FeedManagerMetadataService implements MetadataService {
private static final Logger log = LoggerFactory.getLogger(FeedManagerMetadataService.class);
@Value("${kylo.feed.mgr.cleanup.timeout:60000}")
private long cleanupTimeout;
@Value("${kylo.feed.mgr.cleanup.delay:300}")
private long cleanupDelay;
@Inject
FeedManagerCategoryService categoryProvider;
@Inject
FeedManagerTemplateService templateProvider;
@Inject
FeedManagerFeedService feedProvider;
@Inject
LegacyNifiRestClient nifiRestClient;
@Inject
MetadataAccess metadataAccess;
@Inject
FeedModelTransform feedModelTransform;
@Inject
private AccessController accessController;
// Metadata event service
@Inject
private MetadataEventService eventService;
// I had to use autowired instead of Inject to allow null values.
@Autowired(required = false)
@Qualifier("hadoopAuthorizationService")
private HadoopAuthorizationService hadoopAuthorizationService;
/**
* NiFi REST client
*/
@Inject
private NiFiRestClient nifiClient;
@Override
public boolean checkFeedPermission(String id, Action action, Action... more) {
return feedProvider.checkFeedPermission(id, action, more);
}
@Override
public RegisteredTemplate registerTemplate(RegisteredTemplate registeredTemplate) {
return templateProvider.registerTemplate(registeredTemplate);
}
@Override
public List<NifiProperty> getTemplateProperties(String templateId) {
return templateProvider.getTemplateProperties(templateId);
}
public void deleteRegisteredTemplate(String templateId) {
templateProvider.deleteRegisteredTemplate(templateId);
}
@Override
public List<RegisteredTemplate> getRegisteredTemplates() {
return templateProvider.getRegisteredTemplates();
}
@Override
public RegisteredTemplate findRegisteredTemplateByName(String templateName) {
return templateProvider.findRegisteredTemplateByName(templateName);
}
@Override
public NifiFeed createFeed(FeedMetadata feedMetadata) {
NifiFeed feed = feedProvider.createFeed(feedMetadata);
if (feed.isSuccess()) {
if (feed.isEnableAfterSave()) {
enableFeed(feed.getFeedMetadata().getId());
}
//requery to get the latest version
FeedMetadata updatedFeed = getFeedById(feed.getFeedMetadata().getId());
feed.setFeedMetadata(updatedFeed);
}
return feed;
}
@Override
public void deleteFeed(@Nonnull final String feedId) {
// First check if this should be allowed.
this.accessController.checkPermission(AccessController.SERVICES, FeedServicesAccessControl.ADMIN_FEEDS);
// Step 1: Fetch feed metadata
final FeedMetadata feed = feedProvider.getFeedById(feedId);
if (feed == null) {
throw new IllegalArgumentException("Unknown feed: " + feedId);
}
// Step 2: Check for dependent feeds
if (feed.getUsedByFeeds() != null && !feed.getUsedByFeeds().isEmpty()) {
final List<String> systemNames = feed.getUsedByFeeds().stream().map(FeedSummary::getCategoryAndFeedSystemName).collect(Collectors.toList());
throw new IllegalStateException("Feed is referenced by " + feed.getUsedByFeeds().size() + " other feeds: " + systemNames);
}
// Step 3: Delete hadoop authorization security policies if they exists
if (hadoopAuthorizationService != null) {
metadataAccess.read(() -> {
Feed domainFeed = feedModelTransform.feedToDomain(feed);
String hdfsPaths = (String) domainFeed.getProperties().get(HadoopAuthorizationService.REGISTRATION_HDFS_FOLDERS);
hadoopAuthorizationService.deleteHivePolicy(feed.getSystemCategoryName(), feed.getSystemFeedName());
hadoopAuthorizationService.deleteHdfsPolicy(feed.getSystemCategoryName(), feed.getSystemFeedName(), HadoopAuthorizationService.convertNewlineDelimetedTextToList(hdfsPaths));
});
}
// Step 4: Enable NiFi cleanup flow
boolean needsCleanup = false;
final ProcessGroupDTO feedProcessGroup;
final ProcessGroupDTO categoryProcessGroup = nifiRestClient.getProcessGroupByName("root", feed.getSystemCategoryName(), false, true);
if (categoryProcessGroup != null) {
feedProcessGroup = NifiProcessUtil.findFirstProcessGroupByName(categoryProcessGroup.getContents().getProcessGroups(), feed.getSystemFeedName());
if (feedProcessGroup != null) {
needsCleanup = nifiRestClient.setInputAsRunningByProcessorMatchingType(feedProcessGroup.getId(), "com.thinkbiganalytics.nifi.v2.metadata.TriggerCleanup");
}
}
// Step 5: Run NiFi cleanup flow
if (needsCleanup) {
// Wait for input processor to start
try {
Thread.sleep(cleanupDelay);
} catch (InterruptedException e) {
// ignored
}
cleanupFeed(feed);
}
// Step 6: Remove feed from NiFi
if (categoryProcessGroup != null) {
final Set<ConnectionDTO> connections = categoryProcessGroup.getContents().getConnections();
for (ProcessGroupDTO processGroup : NifiProcessUtil.findProcessGroupsByFeedName(categoryProcessGroup.getContents().getProcessGroups(), feed.getSystemFeedName())) {
nifiRestClient.deleteProcessGroupAndConnections(processGroup, connections);
}
}
// Step 7: Delete database entries
feedProvider.deleteFeed(feedId);
}
/**
* Changes the state of the specified feed.
*
* @param feedSummary the feed
* @param state the new state
* @return {@code true} if the feed is in the new state, or {@code false} otherwise
*/
private boolean updateNifiFeedRunningStatus(FeedSummary feedSummary, Feed.State state) {
// Validate parameters
if (feedSummary == null || !feedSummary.getState().equals(state.name())) {
return false;
}
// Find the process group
final Optional<ProcessGroupDTO> categoryGroup = nifiClient.processGroups().findByName("root", feedSummary.getSystemCategoryName(), false, false);
final Optional<ProcessGroupDTO> feedGroup = categoryGroup.flatMap(group -> nifiClient.processGroups().findByName(group.getId(), feedSummary.getSystemFeedName(), false, true));
if (!feedGroup.isPresent()) {
log.warn("NiFi process group missing for feed: {}.{}", feedSummary.getSystemCategoryName(), feedSummary.getSystemFeedName());
return Feed.State.DISABLED.equals(state);
}
// Update the state
if (state.equals(Feed.State.ENABLED)) {
nifiClient.processGroups().schedule(feedGroup.get().getId(), categoryGroup.get().getId(), NiFiComponentState.RUNNING);
} else if (state.equals(Feed.State.DISABLED)) {
nifiRestClient.stopInputs(feedGroup.get());
}
return true;
}
public FeedSummary enableFeed(String feedId) {
return metadataAccess.commit(() -> {
this.accessController.checkPermission(AccessController.SERVICES, FeedServicesAccessControl.EDIT_FEEDS);
FeedMetadata feedMetadata = feedProvider.getFeedById(feedId);
if (feedMetadata == null) {
//feed will not be found when user is allowed to export feeds but has no entity access to feed with feed id
throw new NotFoundException("Feed not found for id " + feedId);
}
if (!feedMetadata.getState().equals(Feed.State.ENABLED.name())) {
FeedSummary feedSummary = feedProvider.enableFeed(feedId);
boolean updatedNifi = updateNifiFeedRunningStatus(feedSummary, Feed.State.ENABLED);
if (!updatedNifi) {
//rollback
throw new RuntimeException("Unable to enable Feed " + feedId);
}
return feedSummary;
}
return new FeedSummary(feedMetadata);
});
}
public FeedSummary disableFeed(final String feedId) {
return metadataAccess.commit(() -> {
this.accessController.checkPermission(AccessController.SERVICES, FeedServicesAccessControl.EDIT_FEEDS);
FeedMetadata feedMetadata = feedProvider.getFeedById(feedId);
if (feedMetadata == null) {
throw new NotFoundException("Feed not found for id " + feedId);
}
if (!feedMetadata.getState().equals(Feed.State.DISABLED.name())) {
FeedSummary feedSummary = feedProvider.disableFeed(feedId);
boolean updatedNifi = updateNifiFeedRunningStatus(feedSummary, Feed.State.DISABLED);
if (!updatedNifi) {
//rollback
throw new RuntimeException("Unable to disable Feed " + feedId);
}
return feedSummary;
}
return new FeedSummary(feedMetadata);
});
}
@Override
public Collection<FeedMetadata> getFeeds() {
return feedProvider.getFeeds();
}
@Override
public Collection<? extends UIFeed> getFeeds(boolean verbose) {
return feedProvider.getFeeds(verbose);
}
@Override
public List<FeedSummary> getFeedSummaryData() {
return feedProvider.getFeedSummaryData();
}
@Override
public List<FeedSummary> getFeedSummaryForCategory(String categoryId) {
return feedProvider.getFeedSummaryForCategory(categoryId);
}
@Override
public FeedMetadata getFeedByName(String categoryName, String feedName) {
return feedProvider.getFeedByName(categoryName, feedName);
}
@Override
public FeedMetadata getFeedById(String feedId) {
return feedProvider.getFeedById(feedId);
}
@Override
public FeedMetadata getFeedById(String feedId, boolean refreshTargetTableSchema) {
return feedProvider.getFeedById(feedId, refreshTargetTableSchema);
}
@Override
public Collection<FeedCategory> getCategories() {
return categoryProvider.getCategories();
}
@Override
public FeedCategory getCategoryBySystemName(String name) {
return categoryProvider.getCategoryBySystemName(name);
}
@Override
public void saveCategory(FeedCategory category) {
categoryProvider.saveCategory(category);
}
@Override
public boolean deleteCategory(String categoryId) throws InvalidOperationException {
return categoryProvider.deleteCategory(categoryId);
}
/**
* Runs the cleanup flow for the specified feed.
*
* @param feed the feed to be cleaned up
* @throws FeedCleanupFailedException if the cleanup flow was started but failed to complete successfully
* @throws FeedCleanupTimeoutException if the cleanup flow was started but failed to complete in the allotted time
* @throws RuntimeException if the cleanup flow could not be started
*/
private void cleanupFeed(@Nonnull final FeedMetadata feed) {
// Create event listener
final FeedCompletionListener listener = new FeedCompletionListener(feed, Thread.currentThread());
eventService.addListener(listener);
try {
// Trigger cleanup
feedProvider.enableFeedCleanup(feed.getId());
eventService.notify(new CleanupTriggerEvent(feedProvider.resolveFeed(feed.getId())));
// Wait for completion
long remaining = cleanupTimeout;
while (remaining > 0 && (listener.getState() == null || listener.getState() == FeedOperation.State.STARTED)) {
final long start = System.currentTimeMillis();
try {
Thread.sleep(remaining);
} catch (InterruptedException e) {
// ignored
}
remaining -= System.currentTimeMillis() - start;
}
} finally {
eventService.removeListener(listener);
}
// Check result
if (listener.getState() == null || listener.getState() == FeedOperation.State.STARTED) {
throw new FeedCleanupTimeoutException("Cleanup timed out for feed: " + feed.getId());
}
if (listener.getState() != FeedOperation.State.SUCCESS) {
throw new FeedCleanupFailedException("Cleanup state " + listener.getState() + " for feed: " + feed.getId());
}
}
@Nonnull
@Override
public Set<UserProperty> getCategoryUserFields() {
return categoryProvider.getUserProperties();
}
@Nonnull
@Override
public Optional<Set<UserProperty>> getFeedUserFields(@Nonnull final String categoryId) {
return feedProvider.getUserFields(categoryId);
}
@Nonnull
@Override
public UserFieldCollection getUserFields() {
final UserFieldCollection collection = new UserFieldCollection();
collection.setCategoryFields(categoryProvider.getUserFields());
collection.setFeedFields(feedProvider.getUserFields());
return collection;
}
@Override
public void setUserFields(@Nonnull final UserFieldCollection userFields) {
categoryProvider.setUserFields(userFields.getCategoryFields());
feedProvider.setUserFields(userFields.getFeedFields());
}
/**
* Listens for a feed completion then interrupts a target thread.
*/
private static class FeedCompletionListener implements MetadataEventListener<FeedOperationStatusEvent> {
/**
* Name of the feed to watch for
*/
@Nonnull
private final String feedName;
/**
* Thread to interrupt
*/
@Nonnull
private final Thread target;
/**
* Current state of the feed
*/
@Nullable
private FeedOperation.State state;
/**
* Constructs a {@code FeedCompletionListener} that listens for events for the specified feed then interrupts the specified thread.
*
* @param feed the feed to watch far
* @param target the thread to interrupt
*/
FeedCompletionListener(@Nonnull final FeedMetadata feed, @Nonnull final Thread target) {
this.feedName = feed.getCategoryAndFeedName();
this.target = target;
}
/**
* Gets the current state of the feed.
*
* @return the feed state
*/
@Nullable
public FeedOperation.State getState() {
return state;
}
@Override
public void notify(@Nonnull final FeedOperationStatusEvent event) {
if (event.getData().getFeedName().equals(feedName)) {
state = event.getData().getState();
target.interrupt();
}
}
}
}