From 5cf5410bc82dd1ff3fbf03272c39f692df95108b Mon Sep 17 00:00:00 2001
From: Djordje Lukic <djordje.lukic@docker.com>
Date: Sat, 21 Nov 2020 22:57:39 +0100
Subject: [PATCH] Detect cycles

Signed-off-by: Djordje Lukic <djordje.lukic@docker.com>
---
 local/dependencies.go | 61 ++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 58 insertions(+), 3 deletions(-)

diff --git a/local/dependencies.go b/local/dependencies.go
index 344eef918..92237370c 100644
--- a/local/dependencies.go
+++ b/local/dependencies.go
@@ -21,6 +21,7 @@ package local
 import (
 	"context"
 	"fmt"
+	"strings"
 	"sync"
 
 	"github.com/compose-spec/compose-go/types"
@@ -36,6 +37,10 @@ const (
 
 func inDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, types.ServiceConfig) error) error {
 	g := NewGraph(project.Services)
+	if b, err := g.HasCycles(); b {
+		return err
+	}
+
 	leaves := g.Leaves()
 
 	eg, _ := errgroup.WithContext(ctx)
@@ -50,7 +55,7 @@ func inDependencyOrder(ctx context.Context, project *types.Project, fn func(cont
 func run(ctx context.Context, graph *Graph, eg *errgroup.Group, nodes []*Vertex, fn func(context.Context, types.ServiceConfig) error) error {
 	for _, node := range nodes {
 		n := node
-		// Don't start this service yet if all of their children have
+		// Don't start this service yet if all of its children have
 		// not been started yet.
 		if len(graph.FilterChildren(n.Service.Name, ServiceStopped)) != 0 {
 			continue
@@ -152,8 +157,6 @@ func (g *Graph) AddEdge(source string, destination string) error {
 	sourceVertex.Children[destination] = destinationVertex
 	destinationVertex.Parents[source] = sourceVertex
 
-	g.Vertices[source] = sourceVertex
-	g.Vertices[destination] = destinationVertex
 	return nil
 }
 
@@ -192,3 +195,55 @@ func (g *Graph) FilterChildren(key string, status ServiceStatus) []*Vertex {
 
 	return res
 }
+
+func (g *Graph) HasCycles() (bool, error) {
+	discovered := []string{}
+	finished := []string{}
+
+	for _, vertex := range g.Vertices {
+		path := []string{
+			vertex.Key,
+		}
+		if !contains(discovered, vertex.Key) && !contains(finished, vertex.Key) {
+			var err error
+			discovered, finished, err = g.visit(vertex.Key, path, discovered, finished)
+
+			if err != nil {
+				return true, err
+			}
+		}
+	}
+
+	return false, nil
+}
+
+func (g *Graph) visit(key string, path []string, discovered []string, finished []string) ([]string, []string, error) {
+	discovered = append(discovered, key)
+
+	for _, v := range g.Vertices[key].Children {
+		path := append(path, v.Key)
+		if contains(discovered, v.Key) {
+			return nil, nil, fmt.Errorf("cycle found: %s", strings.Join(path, " -> "))
+		}
+
+		if !contains(finished, v.Key) {
+			if _, _, err := g.visit(v.Key, path, discovered, finished); err != nil {
+				return nil, nil, err
+			}
+		}
+	}
+
+	discovered = remove(discovered, key)
+	finished = append(finished, key)
+	return discovered, finished, nil
+}
+
+func remove(slice []string, item string) []string {
+	var s []string
+	for _, i := range slice {
+		if i != item {
+			s = append(s, i)
+		}
+	}
+	return s
+}