package alien4cloud.tosca.parser.postprocess;
import static alien4cloud.utils.AlienUtils.safe;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Resource;
import org.alien4cloud.tosca.model.CSARDependency;
import org.alien4cloud.tosca.model.Csar;
import org.alien4cloud.tosca.model.definitions.AbstractPropertyValue;
import org.alien4cloud.tosca.model.definitions.PropertyDefinition;
import org.alien4cloud.tosca.model.definitions.PropertyValue;
import org.alien4cloud.tosca.model.definitions.RepositoryDefinition;
import org.alien4cloud.tosca.model.types.DataType;
import org.apache.commons.collections.MapUtils;
import org.springframework.stereotype.Component;
import org.yaml.snakeyaml.nodes.Node;
import com.google.common.collect.Sets;
import alien4cloud.tosca.context.ToscaContext;
import alien4cloud.tosca.model.ArchiveRoot;
import alien4cloud.tosca.normative.NormativeCredentialConstant;
import alien4cloud.tosca.parser.ParsingContextExecution;
import alien4cloud.tosca.parser.ParsingError;
import alien4cloud.tosca.parser.ParsingErrorLevel;
import alien4cloud.tosca.parser.impl.ErrorCode;
import alien4cloud.utils.AlienUtils;
import alien4cloud.utils.PropertyUtil;
import alien4cloud.utils.VersionUtil;
/**
* Performs validation and post processing of a TOSCA archive.
*/
@Component
public class ArchiveRootPostProcessor implements IPostProcessor<ArchiveRoot> {
@Resource
private DerivedFromPostProcessor derivedFromPostProcessor;
@Resource
private ToscaTypePostProcessor toscaTypePostProcessor;
@Resource
private NodeTypePostProcessor nodeTypePostProcessor;
@Resource
private ToscaArtifactTypePostProcessor toscaArtifactTypePostProcessor;
@Resource
private TopologyPostProcessor topologyPostProcessor;
@Resource
private PropertyValueChecker propertyValueChecker;
/**
* Perform validation of a Tosca archive.
*
* @param archiveRoot The archive to validate and post process.
*/
public void process(ArchiveRoot archiveRoot) {
// Register the archive in the context to ensure that all types are mapped to the TOSCA context.
// In alien4cloud archives are referenced by id and version however this is not required by TOSCA, the following code set/unset temporary ids and reset
// them to avoid registration issue.
String archiveName = archiveRoot.getArchive().getName();
String archiveVersion = archiveRoot.getArchive().getVersion();
archiveRoot.getArchive().setYamlFilePath(ParsingContextExecution.getFileName());
if (archiveName == null) {
archiveRoot.getArchive().setName(ParsingContextExecution.getFileName());
}
if (archiveVersion == null) {
archiveRoot.getArchive().setVersion("undefined");
}
// All type validation may require local archive types, so we need to register the current archive.
ToscaContext.get().register(archiveRoot);
doProcess(archiveRoot);
// reset to TOSCA template value (in case they where changed)
archiveRoot.getArchive().setName(archiveName);
archiveRoot.getArchive().setVersion(archiveVersion);
}
private void doProcess(ArchiveRoot archiveRoot) {
// Note: no post processing has to be done on repositories
processImports(archiveRoot);
// Post process all types from the archive and update their list of parent as well as merge them with their parent types.
processTypes(archiveRoot);
// Then process the topology
topologyPostProcessor.process(archiveRoot.getTopology());
processRepositoriesDefinitions(archiveRoot.getRepositories());
}
/**
* Process imports within the archive and compute its complete dependency set.
* Resolve all dependency version conflicts using the following rules:
* <ul>
* <li>If two direct dependencies conflict with each other, use the latest version</li>
* <li>If a transitive dependency conflicts with a direct dependency, use the direct dependency version</li>
* <li>If two transitive dependency conflict with each other, use the latest version.</li>
* </ul>
*
* @param archiveRoot The archive to process.
*/
private void processImports(ArchiveRoot archiveRoot) {
if (archiveRoot.getArchive().getDependencies() == null || archiveRoot.getArchive().getDependencies().isEmpty()) {
return;
}
// Dependencies defined in the import section only
// These should override transitive deps regardless of type of conflict ?
Set<CSARDependency> dependencies = archiveRoot.getArchive().getDependencies();
/* Three types of conflicts :
- A transitive dep has a different version than a direct dependency => Force transitive to direct version
- Transitive dependencies with the same name and different version are used => Use latest
- Direct dependencies with the same name and different version are used => Error or use latest ?
*/
// 1. Resolve all direct dependencies using latest version
dependencies.removeIf(dependency ->
dependencyConflictsWithLatest(dependency, dependencies)
);
// Compute all distinct transitives dependencies
final Set<CSARDependency> transitiveDependencies = new HashSet<>(
dependencies.stream()
.map(csarDependency -> ToscaContext.get().getArchive(csarDependency.getName(), csarDependency.getVersion()))
.map(Csar::getDependencies)
.filter(c -> c != null)
.reduce(Sets::union)
.orElse(Collections.emptySet())
);
// 2. Resolve all transitive vs. direct dependencies conflicts using the direct dependency's version
transitiveDependencies.removeIf(transitiveDependency ->
dependencyConflictsWithDirect(transitiveDependency, dependencies)
);
// 3. Resolve all transitive dependencies conflicts using latest version
transitiveDependencies.removeIf(transitiveDependency ->
dependencyConflictsWithLatest(transitiveDependency, transitiveDependencies)
);
// Merge all dependencies (direct + transitives)
final Set<CSARDependency> mergedDependencies = new HashSet<>(Sets.union(dependencies, transitiveDependencies));
archiveRoot.getArchive().setDependencies(mergedDependencies);
// Update Tosca context with the complete dependency set
ToscaContext.get().resetDependencies(mergedDependencies);
}
/**
* Check for dependency conflicts between a transitive and a set of direct dependencies.
*
* @param transitiveDependency The dependency to check
* @param dependencies The set of dependency to validate it against - assuming those are direct dependencies.
* @return <code>true</code> if the given dependency is present in the Set in a different version.
*/
private boolean dependencyConflictsWithDirect(CSARDependency transitiveDependency, Set<CSARDependency> dependencies) {
return dependencies.stream()
.filter(directDep -> Objects.equals(directDep.getName(), transitiveDependency.getName())
&& !Objects.equals(directDep.getVersion(), transitiveDependency.getVersion()))
.findFirst() // As we resolved direct dependencies conflicts earlier, there can only be one direct dependency that conflicts
.map(conflictingDependency -> {
// Log the dependency conflict as a warning.
ParsingContextExecution.getParsingErrors().add(
new ParsingError(ParsingErrorLevel.WARNING, ErrorCode.TRANSITIVE_DEPENDENCY_VERSION_CONFLICT,
AlienUtils.prefixWith(":", conflictingDependency.getVersion(), conflictingDependency.getName()), null,
AlienUtils.prefixWith(":", transitiveDependency.getVersion(), transitiveDependency.getName()),
null, conflictingDependency.getVersion()
)
);
// Resolve conflict by using the direct dependency version - delete the transitive dependency
return true;
}).orElse(false);
}
/**
* Check dependencies for version conflicts, and add a warning if one is found.
*
* @param dependency The dependency to verify.
* @param dependencies The set of dependencies it belongs to.
* @return <code>true</code> if the given dependency is present in the Set in a newer version.
*/
private boolean dependencyConflictsWithLatest(CSARDependency dependency, Set<CSARDependency> dependencies) {
return dependencies.stream()
.anyMatch(csarDependency -> {
if (Objects.equals(dependency.getName(), csarDependency.getName())
&& VersionUtil.compare(dependency.getVersion(), csarDependency.getVersion()) < 0) {
ParsingContextExecution.getParsingErrors()
.add(new ParsingError(ParsingErrorLevel.WARNING, ErrorCode.DEPENDENCY_VERSION_CONFLICT,
AlienUtils.prefixWith(":", dependency.getVersion(), dependency.getName()), null,
AlienUtils.prefixWith(":", csarDependency.getVersion(), csarDependency.getName()),
null, null));
return true;
} else return false;
});
}
private void processRepositoriesDefinitions(Map<String, RepositoryDefinition> repositories) {
if (MapUtils.isNotEmpty(repositories)) {
DataType credentialType = ToscaContext.get(DataType.class, NormativeCredentialConstant.DATA_TYPE);
repositories.values().forEach(repositoryDefinition -> {
if (repositoryDefinition.getCredential() != null) {
credentialType.getProperties().forEach((propertyName, propertyDefinition) -> {
// Fill with default value
if (!repositoryDefinition.getCredential().getValue().containsKey(propertyName)) {
AbstractPropertyValue defaultValue = PropertyUtil.getDefaultPropertyValueFromPropertyDefinition(propertyDefinition);
if (defaultValue instanceof PropertyValue) {
repositoryDefinition.getCredential().getValue().put(propertyName, ((PropertyValue) defaultValue).getValue());
}
}
});
Node credentialNode = ParsingContextExecution.getObjectToNodeMap().get(repositoryDefinition.getCredential());
PropertyDefinition propertyDefinition = new PropertyDefinition();
propertyDefinition.setType(NormativeCredentialConstant.DATA_TYPE);
propertyValueChecker.checkProperty("credential", credentialNode, repositoryDefinition.getCredential(), propertyDefinition,
repositoryDefinition.getId());
}
});
}
}
private void processTypes(ArchiveRoot archiveRoot) {
// First of all we have to manage derived from post processor that will be applied to the map of resources as it manage dependency ordering.
derivedFromPostProcessor.process(archiveRoot.getDataTypes());
derivedFromPostProcessor.process(archiveRoot.getArtifactTypes());
derivedFromPostProcessor.process(archiveRoot.getCapabilityTypes());
derivedFromPostProcessor.process(archiveRoot.getRelationshipTypes());
derivedFromPostProcessor.process(archiveRoot.getNodeTypes());
safe(archiveRoot.getDataTypes()).values().stream().forEach(toscaTypePostProcessor);
safe(archiveRoot.getArtifactTypes()).values().stream().forEach(toscaTypePostProcessor);
safe(archiveRoot.getCapabilityTypes()).values().stream().forEach(toscaTypePostProcessor);
safe(archiveRoot.getRelationshipTypes()).values().stream().peek(toscaTypePostProcessor).forEach(toscaArtifactTypePostProcessor);
safe(archiveRoot.getNodeTypes()).values().stream().peek(toscaTypePostProcessor).peek(nodeTypePostProcessor).forEach(toscaArtifactTypePostProcessor);
}
}