package alien4cloud.topology;
import java.io.IOException;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.BiConsumer;
import java.util.regex.Pattern;
import javax.annotation.Resource;
import javax.inject.Inject;
import org.alien4cloud.tosca.catalog.ArchiveDelegateType;
import org.alien4cloud.tosca.catalog.index.ICsarDependencyLoader;
import org.alien4cloud.tosca.catalog.index.IToscaTypeSearchService;
import org.alien4cloud.tosca.editor.EditionContext;
import org.alien4cloud.tosca.model.CSARDependency;
import org.alien4cloud.tosca.model.Csar;
import org.alien4cloud.tosca.model.templates.NodeTemplate;
import org.alien4cloud.tosca.model.templates.RelationshipTemplate;
import org.alien4cloud.tosca.model.templates.Topology;
import org.alien4cloud.tosca.model.types.AbstractToscaType;
import org.alien4cloud.tosca.model.types.NodeType;
import org.alien4cloud.tosca.model.types.RelationshipType;
import org.alien4cloud.tosca.topology.TopologyDTOBuilder;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.elasticsearch.common.collect.Lists;
import org.elasticsearch.mapping.FilterValuesStrategy;
import org.springframework.stereotype.Service;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import alien4cloud.application.ApplicationService;
import alien4cloud.dao.IGenericSearchDAO;
import alien4cloud.dao.model.GetMultipleDataResult;
import alien4cloud.exception.AlreadyExistException;
import alien4cloud.exception.NotFoundException;
import alien4cloud.exception.VersionConflictException;
import alien4cloud.model.application.Application;
import alien4cloud.security.AuthorizationUtil;
import alien4cloud.security.model.ApplicationRole;
import alien4cloud.security.model.Role;
import alien4cloud.topology.exception.UpdateTopologyException;
import alien4cloud.topology.task.SuggestionsTask;
import alien4cloud.topology.task.TaskCode;
import alien4cloud.tosca.container.ToscaTypeLoader;
import alien4cloud.tosca.context.ToscaContext;
import alien4cloud.tosca.context.ToscaContextual;
import alien4cloud.utils.MapUtil;
import alien4cloud.utils.VersionUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class TopologyService {
@Resource
private IToscaTypeSearchService toscaTypeSearchService;
@Resource(name = "alien-es-dao")
private IGenericSearchDAO alienDAO;
@Resource
private ICsarDependencyLoader csarDependencyLoader;
@Resource
private TopologyServiceCore topologyServiceCore;
@Inject
private ApplicationService appService;
@Inject
private TopologyDTOBuilder topologyDTOBuilder;
public static final Pattern NODE_NAME_PATTERN = Pattern.compile("^\\w+$");
public static final Pattern NODE_NAME_REPLACE_PATTERN = Pattern.compile("\\W");
private ToscaTypeLoader initializeTypeLoader(Topology topology, boolean failOnTypeNotFound) {
// FIXME we should use ToscaContext here, and why not allowing the caller to pass ona Context?
ToscaTypeLoader loader = new ToscaTypeLoader(csarDependencyLoader);
Map<String, NodeType> nodeTypes = topologyServiceCore.getIndexedNodeTypesFromTopology(topology, false, false, failOnTypeNotFound);
Map<String, RelationshipType> relationshipTypes = topologyServiceCore.getIndexedRelationshipTypesFromTopology(topology, failOnTypeNotFound);
if (topology.getNodeTemplates() != null) {
for (NodeTemplate nodeTemplate : topology.getNodeTemplates().values()) {
NodeType nodeType = nodeTypes.get(nodeTemplate.getType());
// just load found types.
// the type might be null when failOnTypeNotFound is set to false.
if (nodeType != null) {
loader.loadType(nodeTemplate.getType(), csarDependencyLoader.buildDependencyBean(nodeType.getArchiveName(), nodeType.getArchiveVersion()));
}
if (nodeTemplate.getRelationships() != null) {
for (RelationshipTemplate relationshipTemplate : nodeTemplate.getRelationships().values()) {
RelationshipType relationshipType = relationshipTypes.get(relationshipTemplate.getType());
// just load found types.
// the type might be null when failOnTypeNotFound is set to false.
if (relationshipType != null) {
loader.loadType(relationshipTemplate.getType(),
csarDependencyLoader.buildDependencyBean(relationshipType.getArchiveName(), relationshipType.getArchiveVersion()));
}
}
}
}
}
if (topology.getSubstitutionMapping() != null && topology.getSubstitutionMapping().getSubstitutionType() != null) {
NodeType substitutionType = topology.getSubstitutionMapping().getSubstitutionType();
loader.loadType(substitutionType.getElementId(),
csarDependencyLoader.buildDependencyBean(substitutionType.getArchiveName(), substitutionType.getArchiveVersion()));
}
return loader;
}
/**
* Find replacements nodes for a node template
*
* @param nodeTemplateName the node to search for replacements
* @param topology the topology
* @return all possible replacement types for this node
*/
@SneakyThrows(IOException.class)
public NodeType[] findReplacementForNode(String nodeTemplateName, Topology topology) {
NodeTemplate nodeTemplate = topology.getNodeTemplates().get(nodeTemplateName);
Map<String, Map<String, Set<String>>> nodeTemplatesToFilters = Maps.newHashMap();
Entry<String, NodeTemplate> nodeTempEntry = Maps.immutableEntry(nodeTemplateName, nodeTemplate);
NodeType indexedNodeType = toscaTypeSearchService.getRequiredElementInDependencies(NodeType.class, nodeTemplate.getType(), topology.getDependencies());
processNodeTemplate(topology, nodeTempEntry, nodeTemplatesToFilters);
List<SuggestionsTask> topoTasks = searchForNodeTypes(topology.getWorkspace(), nodeTemplatesToFilters,
MapUtil.newHashMap(new String[] { nodeTemplateName }, new NodeType[] { indexedNodeType }));
if (CollectionUtils.isEmpty(topoTasks)) {
return null;
}
return topoTasks.get(0).getSuggestedNodeTypes();
}
private void addFilters(String nodeTempName, String filterKey, String filterValueToAdd, Map<String, Map<String, Set<String>>> nodeTemplatesToFilters) {
Map<String, Set<String>> filters = nodeTemplatesToFilters.get(nodeTempName);
if (filters == null) {
filters = Maps.newHashMap();
}
Set<String> filterValues = filters.get(filterKey);
if (filterValues == null) {
filterValues = Sets.newHashSet();
}
filterValues.add(filterValueToAdd);
filters.put(filterKey, filterValues);
nodeTemplatesToFilters.put(nodeTempName, filters);
}
/**
* Process a node template to retrieve filters for node replacements search.
*
* TODO cleanup this method, better return a filter for node rather than adding it to a parameter list.
*
* @param topology The topology for which to search filters.
* @param nodeTempEntry The node template for which to find replacement filter.
* @param nodeTemplatesToFilters The map of filters in which to add the filter for the new Node.
*/
public void processNodeTemplate(final Topology topology, final Entry<String, NodeTemplate> nodeTempEntry,
Map<String, Map<String, Set<String>>> nodeTemplatesToFilters) {
String capabilityFilterKey = "capabilities.type";
String requirementFilterKey = "requirements.type";
NodeTemplate template = nodeTempEntry.getValue();
Map<String, RelationshipTemplate> relTemplates = template.getRelationships();
nodeTemplatesToFilters.put(nodeTempEntry.getKey(), null);
// process the node template source of relationships
if (relTemplates != null && !relTemplates.isEmpty()) {
for (RelationshipTemplate relationshipTemplate : relTemplates.values()) {
addFilters(nodeTempEntry.getKey(), requirementFilterKey, relationshipTemplate.getRequirementType(), nodeTemplatesToFilters);
}
}
// process the node template target of relationships
List<RelationshipTemplate> relTemplatesTargetRelated = topologyServiceCore.getTargetRelatedRelatonshipsTemplate(nodeTempEntry.getKey(),
topology.getNodeTemplates());
for (RelationshipTemplate relationshipTemplate : relTemplatesTargetRelated) {
addFilters(nodeTempEntry.getKey(), capabilityFilterKey, relationshipTemplate.getRequirementType(), nodeTemplatesToFilters);
}
}
private NodeType[] getIndexedNodeTypesFromSearchResponse(final GetMultipleDataResult<NodeType> searchResult, final NodeType toExcludeIndexedNodeType)
throws IOException {
NodeType[] toReturnArray = null;
for (int j = 0; j < searchResult.getData().length; j++) {
NodeType nodeType = searchResult.getData()[j];
if (toExcludeIndexedNodeType == null || !nodeType.getId().equals(toExcludeIndexedNodeType.getId())) {
toReturnArray = ArrayUtils.add(toReturnArray, nodeType);
}
}
return toReturnArray;
}
/**
* Search for nodeTypes given some filters. Apply AND filter strategy when multiple values for a filter key.
*/
public List<SuggestionsTask> searchForNodeTypes(String workspace, Map<String, Map<String, Set<String>>> nodeTemplatesToFilters,
Map<String, NodeType> toExcludeIndexedNodeTypes) throws IOException {
if (nodeTemplatesToFilters == null || nodeTemplatesToFilters.isEmpty()) {
return null;
}
List<SuggestionsTask> toReturnTasks = Lists.newArrayList();
for (Map.Entry<String, Map<String, Set<String>>> nodeTemplatesToFiltersEntry : nodeTemplatesToFilters.entrySet()) {
Map<String, String[]> formattedFilters = Maps.newHashMap();
Map<String, FilterValuesStrategy> filterValueStrategy = Maps.newHashMap();
NodeType[] data = null;
if (nodeTemplatesToFiltersEntry.getValue() != null) {
for (Map.Entry<String, Set<String>> filterEntry : nodeTemplatesToFiltersEntry.getValue().entrySet()) {
formattedFilters.put(filterEntry.getKey(), filterEntry.getValue().toArray(new String[filterEntry.getValue().size()]));
// AND strategy if multiple values
filterValueStrategy.put(filterEntry.getKey(), FilterValuesStrategy.AND);
}
// retrieve only non abstract components
formattedFilters.put("abstract", ArrayUtils.toArray("false"));
// use topology workspace + global workspace
formattedFilters.put("workspace", ArrayUtils.toArray(workspace, "ALIEN_GLOBAL_WORKSPACE"));
GetMultipleDataResult<NodeType> searchResult = alienDAO.search(NodeType.class, null, formattedFilters, filterValueStrategy, 20);
data = getIndexedNodeTypesFromSearchResponse(searchResult, toExcludeIndexedNodeTypes.get(nodeTemplatesToFiltersEntry.getKey()));
}
TaskCode taskCode = data == null || data.length < 1 ? TaskCode.IMPLEMENT : TaskCode.REPLACE;
SuggestionsTask task = new SuggestionsTask();
task.setNodeTemplateName(nodeTemplatesToFiltersEntry.getKey());
task.setComponent(toExcludeIndexedNodeTypes.get(nodeTemplatesToFiltersEntry.getKey()));
task.setCode(taskCode);
task.setSuggestedNodeTypes(data);
toReturnTasks.add(task);
}
return toReturnTasks;
}
/**
* Check that the user has enough rights for a given topology.
*
* @param topology The topology for which to check roles.
* @param applicationRoles The roles required to edit the topology for an application.
*/
private void checkAuthorizations(Topology topology, ApplicationRole[] applicationRoles, Role[] roles) {
Csar relatedCsar = ToscaContext.get().getArchive(topology.getArchiveName(), topology.getArchiveVersion());
if (Objects.equals(relatedCsar.getDelegateType(), ArchiveDelegateType.APPLICATION.toString())) {
String applicationId = relatedCsar.getDelegateId();
Application application = appService.getOrFail(applicationId);
AuthorizationUtil.checkAuthorizationForApplication(application, applicationRoles);
} else {
AuthorizationUtil.checkHasOneRoleIn(roles);
}
}
/**
* Check that the current user can retrieve the given topology.
*
* @param topology The topology that is subject to being updated.
*/
@ToscaContextual
public void checkAccessAuthorizations(Topology topology) {
checkAuthorizations(topology,
new ApplicationRole[] { ApplicationRole.APPLICATION_MANAGER, ApplicationRole.APPLICATION_DEVOPS, ApplicationRole.APPLICATION_USER },
new Role[] { Role.COMPONENTS_BROWSER, Role.ARCHITECT });
}
/**
* Check that the current user can update the given topology.
*
* @param topology The topology that is subject to being updated.
*/
@ToscaContextual
public void checkEditionAuthorizations(Topology topology) {
checkAuthorizations(topology, new ApplicationRole[] { ApplicationRole.APPLICATION_MANAGER, ApplicationRole.APPLICATION_DEVOPS },
new Role[] { Role.ARCHITECT });
}
/**
* Build a node template
*
* @param dependencies the dependencies on which new node will be constructed
* @param indexedNodeType the type of the node
* @param templateToMerge the template that can be used to merge into the new node template
* @return new constructed node template
*/
public NodeTemplate buildNodeTemplate(Set<CSARDependency> dependencies, NodeType indexedNodeType, NodeTemplate templateToMerge) {
return topologyServiceCore.buildNodeTemplate(dependencies, indexedNodeType, templateToMerge);
}
/**
* Load a type into the topology (add dependency for this new type, upgrade if necessary ...)
*
* @param topology the topology
* @param element the element to load
* @param <T> tosca element type
* @return The real loaded element if element given in argument is from older archive than topology's dependencies
*/
@SuppressWarnings("unchecked")
public <T extends AbstractToscaType> T loadType(Topology topology, T element) {
String type = element.getElementId();
String archiveName = element.getArchiveName();
String archiveVersion = element.getArchiveVersion();
CSARDependency toLoadDependency = csarDependencyLoader.buildDependencyBean(archiveName, archiveVersion);
// FIXME Transitive dependencies could change here and thus types be affected ?
ToscaTypeLoader typeLoader = initializeTypeLoader(topology, true);
typeLoader.loadType(type, toLoadDependency);
for (CSARDependency updatedDependency : typeLoader.getLoadedDependencies()) {
ToscaContext.get().updateDependency(updatedDependency);
}
// TODO update csar dependencies also ?
topology.setDependencies(typeLoader.getLoadedDependencies());
return element;
}
public void unloadType(Topology topology, String... types) {
// make sure to set the failOnTypeNotFound to false, to deal with topology recovering when a type is deleted from a dependency
ToscaTypeLoader typeLoader = initializeTypeLoader(topology, false);
for (String type : types) {
typeLoader.unloadType(type);
}
// FIXME if a dependency is just removed don't add it back
for (CSARDependency updatedDependency : typeLoader.getLoadedDependencies()) {
ToscaContext.get().updateDependency(updatedDependency);
}
// TODO update csar dependencies also ?
topology.setDependencies(typeLoader.getLoadedDependencies());
}
/**
* Throw an UpdateTopologyException if the topology is released
*
* @param topology topology to be checked
*/
public void throwsErrorIfReleased(Topology topology) {
if (isReleased(topology)) {
throw new UpdateTopologyException("The topology " + topology.getId() + " cannot be updated because it's released");
}
}
/**
* True when an topology is released.
*/
public boolean isReleased(Topology topology) {
return !VersionUtil.isSnapshot(topology.getArchiveVersion());
}
public void isUniqueNodeTemplateName(Topology topology, String newNodeTemplateName) {
if (topology.getNodeTemplates() != null && topology.getNodeTemplates().containsKey(newNodeTemplateName)) {
log.debug("Add Node Template <{}> impossible (already exists)", newNodeTemplateName);
// a node template already exist with the given name.
throw new AlreadyExistException(
"A node template with the given name " + newNodeTemplateName + " already exists in the topology " + topology.getId() + ".");
}
}
public void updateDependencies(EditionContext context, CSARDependency newDependency) {
final Set<CSARDependency> oldDependencies = new HashSet<>(context.getTopology().getDependencies());
final Set<CSARDependency> newDependencies = csarDependencyLoader.getDependencies(newDependency.getName(), newDependency.getVersion());
newDependencies.add(newDependency);
// Update context with the new dependencies.
newDependencies.forEach(csarDependency -> context.getToscaContext().updateDependency(csarDependency));
// Validate that the dependency change does not induce missing types.
try {
this.checkForMissingTypes(context);
} catch (NotFoundException e) {
// Revert changes made to the Context then throw.
context.getToscaContext().resetDependencies(oldDependencies);
context.getTopology().setDependencies(oldDependencies);
throw new VersionConflictException("Changing the dependency ["+ newDependency.getName() + "] to version ["
+ newDependency.getVersion() + "] induces missing types in the topology. Not found : [" + e.getMessage() + "].", e);
}
// Perform the dependency update on the topology.
context.getTopology().setDependencies(new HashSet<>(context.getToscaContext().getDependencies()));
}
/**
* Check for missing types in the Topology
* @param context
* @throws NotFoundException if the Type is used in the topology and not found in its context.
*/
private void checkForMissingTypes(EditionContext context) {
/* TODO: Cache the result or do not use TopologyDTOBuilder
* because it is then called again at the end of the operation execution (EditorController.execute)
*/
TopologyDTO topologyDTO = topologyDTOBuilder.buildTopologyDTO(context);
topologyDTO.getNodeTypes().forEach(throwTypeNotFound());
topologyDTO.getRelationshipTypes().forEach(throwTypeNotFound());
topologyDTO.getCapabilityTypes().forEach(throwTypeNotFound());
topologyDTO.getDataTypes().forEach(throwTypeNotFound());
}
private BiConsumer<String, AbstractToscaType> throwTypeNotFound() {
return (s, type) -> {
if (type == null) {
throw new NotFoundException(s);
}
};
}
public void rebuildDependencies(Topology topology) {
ToscaTypeLoader typeLoader = initializeTypeLoader(topology, true);
topology.setDependencies(typeLoader.getLoadedDependencies());
}
}