Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dependency validation #54 #67

Merged
merged 8 commits into from
Oct 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ script:
./mvnw -ntp verify -Pprod -DskipTests &&
if [ "$BRANCH" = "master" ]; then
docker build -t tillias/microcatalog . &&
docker tag tillias/microcatalog tillias/microcatalog:$DOCKER_TAG &&
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin &&
docker push tillias/microcatalog &&
./mvnw -ntp com.heroku.sdk:heroku-maven-plugin:2.0.5:deploy-only -Pheroku -Dheroku.buildpacks=heroku/jvm -Dheroku.appName=microcatalog; fi
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
[![Join the chat at https://gitter.im/microservice-catalog/community](https://badges.gitter.im/microservice-catalog/community.svg)](https://gitter.im/microservice-catalog/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Build Status](https://travis-ci.org/tillias/microservice-catalog.svg?branch=master)](https://travis-ci.org/tillias/microservice-catalog)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=microcatalog&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=microcatalog)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=microcatalog&metric=coverage)](https://sonarcloud.io/dashboard?id=microcatalog)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=microcatalog&metric=alert_status)](https://sonarcloud.io/dashboard?id=microcatalog)
[![Docker Image Version (latest by date)](https://img.shields.io/docker/v/tillias/microcatalog)](https://hub.docker.com/r/tillias/microcatalog)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=microcatalog&metric=ncloc)](https://sonarcloud.io/dashboard?id=microcatalog)
Expand Down
14 changes: 14 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
<properties-maven-plugin.version>1.0.0</properties-maven-plugin.version>
<sonar-maven-plugin.version>3.7.0.1746</sonar-maven-plugin.version>
<jgrapht.version>1.5.0</jgrapht.version>
<assertj.version>3.18.0</assertj.version>
<jacoco.utReportFolder>${project.build.directory}/jacoco/test</jacoco.utReportFolder>
<jacoco.utReportFile>${jacoco.utReportFolder}/test.exec</jacoco.utReportFile>
<jacoco.itReportFolder>${project.build.directory}/jacoco/integrationTest</jacoco.itReportFolder>
Expand Down Expand Up @@ -109,6 +110,19 @@
<artifactId>jgrapht-core</artifactId>
<version>${jgrapht.version}</version>
</dependency>
<dependency>
<groupId>org.jgrapht</groupId>
<artifactId>jgrapht-io</artifactId>
<version>${jgrapht.version}</version>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>


<dependency>
<groupId>io.github.jhipster</groupId>
<artifactId>jhipster-framework</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ private String resolvePathPrefix() {
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = jHipsterProperties.getCors();
if (config.getAllowedOrigins() != null && !config.getAllowedOrigins().isEmpty()) {
List<String> allowedOrigins = config.getAllowedOrigins();
if (allowedOrigins != null && !allowedOrigins.isEmpty()) {
log.debug("Registering CORS filter");
source.registerCorsConfiguration("/api/**", config);
source.registerCorsConfiguration("/management/**", config);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.github.microcatalog.service.custom;

import com.github.microcatalog.domain.Dependency;
import com.github.microcatalog.domain.Microservice;
import com.github.microcatalog.repository.DependencyRepository;
import com.github.microcatalog.service.custom.exceptions.CircularDependenciesException;
import com.github.microcatalog.service.custom.exceptions.SelfCircularException;
import org.jgrapht.Graph;
import org.jgrapht.alg.cycle.CycleDetector;
import org.jgrapht.graph.DefaultEdge;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

@Service
@Transactional
public class DependencyService {

private final Logger log = LoggerFactory.getLogger(DependencyService.class);

private final GraphLoaderService graphLoaderService;
private final DependencyRepository repository;

public DependencyService(GraphLoaderService graphLoaderService, DependencyRepository repository) {
this.graphLoaderService = graphLoaderService;
this.repository = repository;
}

public Dependency create(final Dependency dependency) {
if (dependency.getId() != null) {
throw new IllegalArgumentException("A new dependency can not already have an id");
}

validateSelfCycle(dependency);
validateIfAdded(dependency);

return repository.save(dependency);
}

public Dependency update(final Dependency dependency) {
if (dependency.getId() == null) {
throw new IllegalArgumentException("Updating non-persistent entity without id");
}

validateSelfCycle(dependency);
validateIfUpdated(dependency);

return repository.save(dependency);
}

public List<Dependency> findAll() {
return repository.findAll();
}

public Optional<Dependency> findById(Long id) {
return repository.findById(id);
}

public void deleteById(Long id) {
repository.deleteById(id);
}

private void validateSelfCycle(final Dependency dependency) {
if (dependency == null) {
return;
}

if (dependency.getSource() == null || dependency.getTarget() == null) {
return;
}

final Microservice source = dependency.getSource();
if (Objects.equals(source.getId(), dependency.getTarget().getId())) {
throw new SelfCircularException("Source of dependency can't be the same as target", source);
}
}

private void validateIfAdded(final Dependency toBeAdded) {
final Graph<Microservice, DefaultEdge> graph = graphLoaderService.loadGraph();
graph.addEdge(toBeAdded.getSource(), toBeAdded.getTarget());

checkCycles(graph);
}

private void validateIfUpdated(final Dependency dependency) {
final Graph<Microservice, DefaultEdge> graph = graphLoaderService.loadGraph();
final Dependency persistent = repository.findById(dependency.getId())
.orElseThrow(() -> new IllegalArgumentException("Trying to update dependency, but it was removed from data source"));

// check what will be if we remove existing edge and replace it with updated one

final DefaultEdge currentEdge = graph.getEdge(persistent.getSource(), persistent.getTarget());
graph.removeEdge(currentEdge);

graph.addEdge(dependency.getSource(), dependency.getTarget());

checkCycles(graph);
}

private void checkCycles(final Graph<Microservice, DefaultEdge> graph) {
final CycleDetector<Microservice, DefaultEdge> cycleDetector = new CycleDetector<>(graph);
if (cycleDetector.detectCycles()) {
final Set<Microservice> cycles = cycleDetector.findCycles();
log.debug("Cycles: {}", cycles);

throw new CircularDependenciesException("Circular dependency will be introduced", cycles);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.github.microcatalog.service.custom;

import com.github.microcatalog.domain.Dependency;
import com.github.microcatalog.domain.Microservice;
import com.github.microcatalog.repository.DependencyRepository;
import com.github.microcatalog.repository.MicroserviceRepository;
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultDirectedGraph;
import org.jgrapht.graph.DefaultEdge;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.List;

@Service
@Transactional
public class GraphLoaderService {

private final MicroserviceRepository microserviceRepository;
private final DependencyRepository dependencyRepository;

public GraphLoaderService(MicroserviceRepository microserviceRepository, DependencyRepository dependencyRepository) {
this.microserviceRepository = microserviceRepository;
this.dependencyRepository = dependencyRepository;
}

public Graph<Microservice, DefaultEdge> loadGraph() {
List<Microservice> microservices = microserviceRepository.findAll();
List<Dependency> dependencies = dependencyRepository.findAll();

return createGraph(microservices, dependencies);
}

private Graph<Microservice, DefaultEdge> createGraph(final List<Microservice> nodes, final List<Dependency> edges) {
final Graph<Microservice, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);

nodes.forEach(graph::addVertex);
edges.forEach(d -> graph.addEdge(d.getSource(), d.getTarget()));

return graph;
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
package com.github.microcatalog.service.custom;

import com.github.microcatalog.domain.*;
import com.github.microcatalog.domain.Microservice;
import com.github.microcatalog.domain.custom.ReleaseGroup;
import com.github.microcatalog.domain.custom.ReleasePath;
import com.github.microcatalog.domain.custom.ReleaseStep;
import com.github.microcatalog.repository.DependencyRepository;
import com.github.microcatalog.repository.MicroserviceRepository;
import org.jgrapht.Graph;
import org.jgrapht.alg.connectivity.ConnectivityInspector;
import org.jgrapht.alg.cycle.CycleDetector;
import org.jgrapht.graph.AsSubgraph;
import org.jgrapht.graph.DefaultDirectedGraph;
import org.jgrapht.graph.DefaultEdge;
import org.jgrapht.graph.EdgeReversedGraph;
import org.jgrapht.traverse.DepthFirstIterator;
Expand All @@ -33,79 +30,67 @@ public class ReleasePathCustomService {

private final Logger log = LoggerFactory.getLogger(ReleasePathCustomService.class);

private final MicroserviceRepository microserviceRepository;
private final DependencyRepository dependencyRepository;
private final GraphLoaderService graphLoaderService;

public ReleasePathCustomService(MicroserviceRepository microserviceRepository,
DependencyRepository dependencyRepository) {
this.microserviceRepository = microserviceRepository;
this.dependencyRepository = dependencyRepository;
public ReleasePathCustomService(GraphLoaderService graphLoaderService) {
this.graphLoaderService = graphLoaderService;
}


public Optional<ReleasePath> getReleasePath(final Long microserviceId) {
final List<Microservice> microservices = microserviceRepository.findAll();
final List<Dependency> dependencies = dependencyRepository.findAll();
final Optional<Microservice> maybeTarget =
microservices.stream().filter(m -> Objects.equals(m.getId(), microserviceId)).findFirst();
final Graph<Microservice, DefaultEdge> graph = graphLoaderService.loadGraph();
final Microservice target = new Microservice();
target.setId(microserviceId);

if (!maybeTarget.isPresent()) {
// can't build release path, cause microservice with given id is not present in graph
if (!graph.containsVertex(target)) {
return Optional.empty();
}

final Graph<Long, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);

microservices.forEach(m -> graph.addVertex(m.getId()));
dependencies.forEach(d -> graph.addEdge(d.getSource().getId(), d.getTarget().getId()));

final ConnectivityInspector<Long, DefaultEdge> inspector = new ConnectivityInspector<>(graph);
final Microservice target = maybeTarget.get();
final Set<Long> connectedSet = inspector.connectedSetOf(target.getId());
final ConnectivityInspector<Microservice, DefaultEdge> inspector = new ConnectivityInspector<>(graph);
final Set<Microservice> connectedSet = inspector.connectedSetOf(target);

// Connected subgraph, that contains target microservice
final AsSubgraph<Long, DefaultEdge> targetSubgraph = new AsSubgraph<>(graph, connectedSet);
final AsSubgraph<Microservice, DefaultEdge> targetSubgraph = new AsSubgraph<>(graph, connectedSet);
log.debug("Connected subgraph, that contains target microservice: {}", targetSubgraph);

final CycleDetector<Long, DefaultEdge> cycleDetector = new CycleDetector<>(targetSubgraph);
final CycleDetector<Microservice, DefaultEdge> cycleDetector = new CycleDetector<>(targetSubgraph);
if (cycleDetector.detectCycles()) {
final Set<Long> cycles = cycleDetector.findCycles();
final Set<Microservice> cycles = cycleDetector.findCycles();
throw new IllegalArgumentException(String.format("There are cyclic dependencies between microservices : %s", cycles));
}

final Set<Long> pathMicroservices = new HashSet<>();
GraphIterator<Long, DefaultEdge> iterator = new DepthFirstIterator<>(targetSubgraph, target.getId());
final Set<Microservice> pathMicroservices = new HashSet<>();
GraphIterator<Microservice, DefaultEdge> iterator = new DepthFirstIterator<>(targetSubgraph, target);
while (iterator.hasNext()) {
pathMicroservices.add(iterator.next());
}

// TODO new use-case and visualisation
// For each element of pathSet calculate all nodes who depends on items from pathSet. Microservices which will be possibly affected if we build target and it's direct dependencies

final Graph<Long, DefaultEdge> pathGraph = new AsSubgraph<>(targetSubgraph, pathMicroservices);
final Graph<Microservice, DefaultEdge> pathGraph = new AsSubgraph<>(targetSubgraph, pathMicroservices);
log.debug("Connected subgraph, which contains all paths from target microservice to it's dependencies {}", pathGraph);

final Graph<Long, DefaultEdge> reversed = new EdgeReversedGraph<>(pathGraph);
final Graph<Long, DefaultEdge> reversedCopy = new AsSubgraph<>(reversed);

return Optional.of(convert(reversedCopy, microservices, target));
final Graph<Microservice, DefaultEdge> reversed = new EdgeReversedGraph<>(pathGraph);
return Optional.of(convert(reversed, target));
}

private ReleasePath convert(final Graph<Long, DefaultEdge> graph, final List<Microservice> microservices, final Microservice target) {
private ReleasePath convert(final Graph<Microservice, DefaultEdge> graph, final Microservice target) {
final ReleasePath result = new ReleasePath();
result.setCreatedOn(Instant.now());
result.setTarget(target);

final List<ReleaseGroup> groups = new ArrayList<>();
final Map<Long, Microservice> microserviceMap = microservices.stream()
.collect(Collectors.toMap(Microservice::getId, m -> m));

do {
final List<Long> verticesWithoutIncomingEdges = graph.vertexSet().stream()
final List<Microservice> verticesWithoutIncomingEdges = graph.vertexSet().stream()
.filter(v -> graph.incomingEdgesOf(v).isEmpty())
.collect(Collectors.toList());
log.debug("Leaves: {}", verticesWithoutIncomingEdges);

final ReleaseGroup group = new ReleaseGroup();
group.setSteps(convertSteps(microserviceMap, verticesWithoutIncomingEdges, graph));
group.setSteps(convertSteps(verticesWithoutIncomingEdges, graph));
groups.add(group);

verticesWithoutIncomingEdges.forEach(graph::removeVertex);
Expand All @@ -116,23 +101,22 @@ private ReleasePath convert(final Graph<Long, DefaultEdge> graph, final List<Mic
return result;
}

private Set<ReleaseStep> convertSteps(final Map<Long, Microservice> microserviceMap,
final List<Long> microserviceIds,
final Graph<Long, DefaultEdge> graph) {
private Set<ReleaseStep> convertSteps(final List<Microservice> verticesWithoutIncomingEdges,
final Graph<Microservice, DefaultEdge> graph) {
final Set<ReleaseStep> result = new HashSet<>();

microserviceIds.forEach(id -> {
verticesWithoutIncomingEdges.forEach(microservice -> {
final List<Microservice> parentWorkItems = new ArrayList<>();

final Set<DefaultEdge> outgoingEdges = graph.outgoingEdgesOf(id);
final Set<DefaultEdge> outgoingEdges = graph.outgoingEdgesOf(microservice);
for (DefaultEdge e : outgoingEdges) {
final Long edgeTarget = graph.getEdgeTarget(e);
parentWorkItems.add(microserviceMap.get(edgeTarget));
final Microservice edgeTarget = graph.getEdgeTarget(e);
parentWorkItems.add(edgeTarget);
}

result.add(
new ReleaseStep()
.workItem(microserviceMap.get(id))
.workItem(microservice)
.parentWorkItems(parentWorkItems)
);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.github.microcatalog.service.custom.exceptions;

import com.github.microcatalog.domain.Microservice;

import java.util.Set;

/**
* Occurs when adding new dependency will introduce cycle
*/
public class CircularDependenciesException extends RuntimeException {
private final Set<Microservice> cycles;

public CircularDependenciesException(String message, Set<Microservice> cycles) {
super(message);

this.cycles = cycles;
}

public Set<Microservice> getCycles() {
return cycles;
}
}
Loading