package org.alien4cloud.tosca.catalog.index;
import static alien4cloud.dao.FilterUtil.singleKeyFilter;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Resource;
import javax.inject.Inject;
import org.alien4cloud.tosca.catalog.events.AfterArchiveIndexed;
import org.alien4cloud.tosca.catalog.events.BeforeArchiveIndexed;
import org.alien4cloud.tosca.catalog.repository.ICsarRepositry;
import org.alien4cloud.tosca.editor.services.TopologySubstitutionService;
import org.alien4cloud.tosca.exporter.ArchiveExportService;
import org.alien4cloud.tosca.model.CSARDependency;
import org.alien4cloud.tosca.model.Csar;
import org.alien4cloud.tosca.model.templates.Topology;
import org.alien4cloud.tosca.model.types.*;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import alien4cloud.component.repository.exception.CSARUsedInActiveDeployment;
import alien4cloud.component.repository.exception.ToscaTypeAlreadyDefinedInOtherCSAR;
import alien4cloud.dao.IGenericSearchDAO;
import alien4cloud.deployment.DeploymentService;
import alien4cloud.exception.AlreadyExistException;
import alien4cloud.model.components.CSARSource;
import alien4cloud.paas.wf.WorkflowsBuilderService;
import alien4cloud.topology.TopologyServiceCore;
import alien4cloud.tosca.context.ToscaContext;
import alien4cloud.tosca.model.ArchiveRoot;
import alien4cloud.tosca.parser.ParsingError;
import alien4cloud.tosca.parser.ParsingErrorLevel;
import alien4cloud.tosca.parser.impl.ErrorCode;
import alien4cloud.utils.VersionUtil;
import lombok.extern.slf4j.Slf4j;
/**
* <p>
* Archive indexed is responsible for indexing a TOSCA Cloud Service Archive out of parsing or created from API.
* </p>
* <p>
* It will split the elements of the archive (Tosca Types, Topology, Csar - archive metadata) to multiple objects stored in various indexes.
* </p>
* <p>
* The archive indexer is also responsible for storing or initializing the file repository for the archive and eventually (if the archive is a SNAPSHOT) the
* local git for editor purpose.
* </p>
*/
@Slf4j
@Component
public class ArchiveIndexer {
@Resource(name = "alien-es-dao")
private IGenericSearchDAO alienDAO;
@Inject
private ApplicationEventPublisher publisher;
@Inject
private ArchiveExportService exportService;
@Inject
private ArchiveImageLoader imageLoader;
@Inject
private ICsarRepositry archiveRepositry;
@Inject
private CsarService csarService;
@Inject
private TopologyServiceCore topologyServiceCore;
@Inject
private IToscaTypeIndexerService indexerService;
@Inject
private WorkflowsBuilderService workflowBuilderService;
@Inject
private DeploymentService deploymentService;
@Inject
private TopologySubstitutionService topologySubstitutionService;
@Inject
private IArchiveIndexerAuthorizationFilter archiveIndexerAuthorizationFilter;
/**
* Check that a CSAR name/version does not already exists in the repository and eventually throw an AlreadyExistException.
*
* @param name The name of the archive.
* @param version The version of the archive.
*/
public void ensureUniqueness(String name, String version) {
long count = csarService.count(singleKeyFilter("version", version), name);
if (count > 0) {
throw new AlreadyExistException("CSAR: " + name + ", Version: " + version + " already exists in the repository.");
}
}
/**
* <p>
* Import a new empty archive with a topology.
* </p>
* <p>
* Note: this archive is not created from parsing but from alien4cloud API. This service will index the archive and topology as well as initialize the file
* repository and tosca yaml.
* </p>
* <p>
* This method cannot be used to override a topology, even a SNAPSHOT as any update to a topology from the API MUST be done through the editor.
* </p>
*
* @param csar The archive to be imported.
* @param topology The topology to be part of the topology.
*/
public synchronized void importNewArchive(Csar csar, Topology topology) {
ArchiveRoot archiveRoot = new ArchiveRoot();
archiveRoot.setArchive(csar);
archiveRoot.setTopology(topology);
// dispatch event before indexing
publisher.publishEvent(new BeforeArchiveIndexed(this, archiveRoot));
// Ensure that the archive does not already exists
ensureUniqueness(csar.getName(), csar.getVersion());
workflowBuilderService.initWorkflows(workflowBuilderService.buildTopologyContext(topology));
// generate the initial yaml in a temporary directory
if (csar.getYamlFilePath() == null) {
csar.setYamlFilePath("topology.yml");
}
String yaml = exportService.getYaml(csar, topology);
// index the archive and topology
csarService.save(csar);
topologyServiceCore.save(topology);
// Initialize the file repository for the archive
archiveRepositry.storeCSAR(csar, yaml);
// dispatch event after indexing
publisher.publishEvent(new AfterArchiveIndexed(this, archiveRoot));
}
/**
* Import a pre-parsed archive to alien 4 cloud indexed catalog.
*
* @param source the source of the archive (alien, orchestrator, upload, git).
* @param archiveRoot The parsed archive object.
* @param archivePath The optional path of the archive (should be null if the archive has been java-generated and not parsed).
* @param parsingErrors The non-null list of parsing errors in which to add errors.
* @throws CSARUsedInActiveDeployment
*/
public synchronized void importArchive(final ArchiveRoot archiveRoot, CSARSource source, Path archivePath, List<ParsingError> parsingErrors)
throws CSARUsedInActiveDeployment, ToscaTypeAlreadyDefinedInOtherCSAR {
archiveIndexerAuthorizationFilter.checkAuthorization(archiveRoot);
String archiveName = archiveRoot.getArchive().getName();
String archiveVersion = archiveRoot.getArchive().getVersion();
Csar currentIndexedArchive = csarService.get(archiveName, archiveVersion);
if (currentIndexedArchive != null) {
if (Objects.equals(currentIndexedArchive.getWorkspace(), archiveRoot.getArchive().getWorkspace())) {
if (currentIndexedArchive.getHash() != null && currentIndexedArchive.getHash().equals(archiveRoot.getArchive().getHash())) {
// if the archive has not changed do nothing.
parsingErrors.add(new ParsingError(ParsingErrorLevel.INFO, ErrorCode.CSAR_ALREADY_INDEXED, "", null,
"The archive already exists in alien4cloud with an identical content (SHA-1 on archive content excluding hidden files is identical).",
null, archiveName));
return;
}
} else {
// If the archive existed in a different workspace then throw error
parsingErrors.add(new ParsingError(ParsingErrorLevel.ERROR, ErrorCode.CSAR_ALREADY_EXISTS_IN_ANOTHER_WORKSPACE, "", null,
"The archive already exists in alien4cloud in a different workspace.", null, archiveName));
return;
}
}
// dispatch event before indexing
publisher.publishEvent(new BeforeArchiveIndexed(this, archiveRoot));
// Throw an exception if we are trying to override a released (non SNAPSHOT) version.
checkNotReleased(currentIndexedArchive);
// In the current version of alien4cloud we must prevent from overriding an archive that is used in a deployment as we still use catalog information at
// runtime.
checkNotUsedInActiveDeployment(currentIndexedArchive);
// FIXME If the archive already exists but can be indexed we should actually call an editor operation to keep git tracking, or should we just prevent
// that ?
checkIfToscaTypesAreDefinedInOtherArchive(archiveRoot);
// save the archive (before we index and save other data so we can cleanup if anything goes wrong).
if (source == null) {
source = CSARSource.OTHER;
}
archiveRoot.getArchive().setImportSource(source.name());
// TODO load transitives dependencies here before saving, as it is not done when parsing
csarService.save(archiveRoot.getArchive());
log.debug("Imported archive {}", archiveRoot.getArchive().getId());
// save the archive in the repository
archiveRepositry.storeCSAR(archiveRoot.getArchive(), archivePath);
// manage images before archive storage in the repository
imageLoader.importImages(archivePath, archiveRoot, parsingErrors);
// index the archive content in elastic-search
indexArchiveTypes(archiveName, archiveVersion, archiveRoot.getArchive().getWorkspace(), archiveRoot, currentIndexedArchive);
indexTopology(archiveRoot, parsingErrors, archiveName, archiveVersion);
publisher.publishEvent(new AfterArchiveIndexed(this, archiveRoot));
}
/**
* Fail if at least one tosca type defined in the archive is already define in an other archive.
*
* @param archiveRoot
* @throws ToscaTypeAlreadyDefinedInOtherCSAR
*/
private void checkIfToscaTypesAreDefinedInOtherArchive(final ArchiveRoot archiveRoot) throws ToscaTypeAlreadyDefinedInOtherCSAR {
failIfOneToscaTypesIsDefinedInOtherArchive(archiveRoot.getNodeTypes());
failIfOneToscaTypesIsDefinedInOtherArchive(archiveRoot.getRelationshipTypes());
failIfOneToscaTypesIsDefinedInOtherArchive(archiveRoot.getCapabilityTypes());
failIfOneToscaTypesIsDefinedInOtherArchive(archiveRoot.getArtifactTypes());
failIfOneToscaTypesIsDefinedInOtherArchive(archiveRoot.getDataTypes());
}
private void failIfOneToscaTypesIsDefinedInOtherArchive(Map<String, ? extends AbstractToscaType> toscaTypes) throws ToscaTypeAlreadyDefinedInOtherCSAR {
if (toscaTypes == null) {
return;
}
for (AbstractToscaType toscaType : toscaTypes.values()) {
failIfToscaTypeIsDefinedInOtherArchive(toscaType);
}
}
private void failIfToscaTypeIsDefinedInOtherArchive(AbstractToscaType toscaType) throws ToscaTypeAlreadyDefinedInOtherCSAR {
if (toscaType == null) {
return;
}
AbstractToscaType indexedNodeType = alienDAO.findById(AbstractToscaType.class, toscaType.getId());
if (indexedNodeType != null && !toscaType.getArchiveName().equals(indexedNodeType.getArchiveName())) {
throw new ToscaTypeAlreadyDefinedInOtherCSAR("Tosca type: " + toscaType.getElementId() + ", version: " + toscaType.getArchiveVersion());
}
}
private void indexTopology(final ArchiveRoot archiveRoot, List<ParsingError> parsingErrors, String archiveName, String archiveVersion) {
Topology topology = archiveRoot.getTopology();
if (topology == null || topology.isEmpty()) {
return;
}
if (archiveRoot.hasToscaTypes()) {
// The archive contains types, we assume those types are used in the embedded topology so we add the dependency to this CSAR
CSARDependency selfDependency = new CSARDependency(archiveRoot.getArchive().getName(), archiveRoot.getArchive().getVersion(),
archiveRoot.getArchive().getHash());
topology.getDependencies().add(selfDependency);
}
// init the workflows
WorkflowsBuilderService.TopologyContext topologyContext = workflowBuilderService
.buildCachedTopologyContext(new WorkflowsBuilderService.TopologyContext() {
@Override
public Topology getTopology() {
return topology;
}
@Override
public <T extends AbstractToscaType> T findElement(Class<T> clazz, String elementId) {
return ToscaContext.get(clazz, elementId);
}
});
workflowBuilderService.initWorkflows(topologyContext);
// FIXME query to check if a previous topology exist for this archive name/version/workspace.
// parsingErrors
// .add(new ParsingError(ParsingErrorLevel.INFO, ErrorCode.TOPOLOGY_UPDATED, "", null, "A topology template has been updated", null, archiveName));
parsingErrors.add(
new ParsingError(ParsingErrorLevel.INFO, ErrorCode.TOPOLOGY_DETECTED, "", null, "A topology template has been detected", null, archiveName));
topologyServiceCore.saveTopology(topology);
topologySubstitutionService.updateSubstitutionType(topology, archiveRoot.getArchive());
}
private void checkNotUsedInActiveDeployment(Csar csar) throws CSARUsedInActiveDeployment {
if (csar != null && deploymentService.isArchiveDeployed(csar.getName(), csar.getVersion())) {
throw new CSARUsedInActiveDeployment("CSAR: " + csar.getName() + ", Version: " + csar.getVersion() + " is used in an active deployment.");
}
}
private void checkNotReleased(Csar archive) {
if (archive != null && !VersionUtil.isSnapshot(archive.getVersion())) {
throw new AlreadyExistException("CSAR: " + archive.getName() + ", Version: " + archive.getVersion() + " already exists in the repository.");
}
}
/**
* Index an archive in Alien indexed repository.
*
* @param archiveName The name of the archive.
* @param archiveVersion The version of the archive.
* @param root The archive root.
* @param archive The previous archive that must be replaced if any.
*/
private void indexArchiveTypes(String archiveName, String archiveVersion, String workspace, ArchiveRoot root, Csar archive) {
if (archive != null) {
// get element from the archive so we get the creation date.
Map<String, AbstractToscaType> previousElements = indexerService.getArchiveElements(archiveName, archiveVersion);
prepareForUpdate(root, previousElements);
// delete the all objects related to the previous archive .
csarService.deleteCsarContent(archive);
}
performIndexing(root);
}
private void prepareForUpdate(ArchiveRoot root, Map<String, AbstractToscaType> previousElements) {
updateCreationDates(root.getArtifactTypes(), previousElements);
updateCreationDates(root.getCapabilityTypes(), previousElements);
updateCreationDates(root.getNodeTypes(), previousElements);
updateCreationDates(root.getRelationshipTypes(), previousElements);
updateCreationDates(root.getDataTypes(), previousElements);
if (root.getLocalImports() != null) {
for (ArchiveRoot child : root.getLocalImports()) {
prepareForUpdate(child, previousElements);
}
}
}
private void updateCreationDates(Map<String, ? extends AbstractInheritableToscaType> newElements, Map<String, AbstractToscaType> previousElements) {
if (newElements == null) {
return;
}
for (AbstractInheritableToscaType newElement : newElements.values()) {
AbstractToscaType previousElement = previousElements.get(newElement.getId());
if (previousElement != null) {
newElement.setCreationDate(previousElement.getCreationDate());
}
}
}
private void performIndexing(ArchiveRoot root) {
indexerService.indexInheritableElements(root.getArtifactTypes(), root.getArchive().getDependencies());
indexerService.indexInheritableElements(root.getCapabilityTypes(), root.getArchive().getDependencies());
indexerService.indexInheritableElements(root.getNodeTypes(), root.getArchive().getDependencies());
indexerService.indexInheritableElements(root.getRelationshipTypes(), root.getArchive().getDependencies());
indexerService.indexInheritableElements(root.getDataTypes(), root.getArchive().getDependencies());
if (root.getLocalImports() != null) {
for (ArchiveRoot child : root.getLocalImports()) {
performIndexing(child);
}
}
}
}