Skip to content

Commit b0f3901

Browse files
committed
introduce CheckCycle to detect cycles in project's depends_on
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent 2440fb0 commit b0f3901

File tree

3 files changed

+64
-44
lines changed

3 files changed

+64
-44
lines changed

graph/cycle.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
Copyright 2020 The Compose Specification Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package graph
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
23+
"github.com/compose-spec/compose-go/v2/types"
24+
"github.com/compose-spec/compose-go/v2/utils"
25+
"golang.org/x/exp/slices"
26+
)
27+
28+
// CheckCycle analyze project's depends_on relation and report an error on cycle detection
29+
func CheckCycle(project *types.Project) error {
30+
g, err := newGraph(project)
31+
if err != nil {
32+
return err
33+
}
34+
return g.checkCycle()
35+
}
36+
37+
func (g *graph[T]) checkCycle() error {
38+
// iterate on vertices in a name-order to render a predicable error message
39+
// this is required by tests and enforce command reproducibility by user, which otherwise could be confusing
40+
names := utils.MapKeys(g.vertices)
41+
for _, name := range names {
42+
err := searchCycle([]string{name}, g.vertices[name])
43+
if err != nil {
44+
return err
45+
}
46+
}
47+
return nil
48+
}
49+
50+
func searchCycle[T any](path []string, v *vertex[T]) error {
51+
names := utils.MapKeys(v.children)
52+
for _, name := range names {
53+
if i := slices.Index(path, name); i > 0 {
54+
return fmt.Errorf("dependency cycle detected: %s", strings.Join(path[i:], " -> "))
55+
}
56+
ch := v.children[name]
57+
err := searchCycle(append(path, name), ch)
58+
if err != nil {
59+
return err
60+
}
61+
}
62+
return nil
63+
}

graph/graph.go

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,6 @@
1616

1717
package graph
1818

19-
import (
20-
"fmt"
21-
"strings"
22-
23-
"github.com/compose-spec/compose-go/v2/utils"
24-
"golang.org/x/exp/slices"
25-
)
26-
2719
// graph represents project as service dependencies
2820
type graph[T any] struct {
2921
vertices map[string]*vertex[T]
@@ -72,34 +64,6 @@ func (g *graph[T]) leaves() []*vertex[T] {
7264
return res
7365
}
7466

75-
func (g *graph[T]) checkCycle() error {
76-
// iterate on vertices in a name-order to render a predicable error message
77-
// this is required by tests and enforce command reproducibility by user, which otherwise could be confusing
78-
names := utils.MapKeys(g.vertices)
79-
for _, name := range names {
80-
err := searchCycle([]string{name}, g.vertices[name])
81-
if err != nil {
82-
return err
83-
}
84-
}
85-
return nil
86-
}
87-
88-
func searchCycle[T any](path []string, v *vertex[T]) error {
89-
names := utils.MapKeys(v.children)
90-
for _, name := range names {
91-
if i := slices.Index(path, name); i > 0 {
92-
return fmt.Errorf("dependency cycle detected: %s", strings.Join(path[i:], " -> "))
93-
}
94-
ch := v.children[name]
95-
err := searchCycle(append(path, name), ch)
96-
if err != nil {
97-
return err
98-
}
99-
}
100-
return nil
101-
}
102-
10367
// descendents return all descendents for a vertex, might contain duplicates
10468
func (v *vertex[T]) descendents() []string {
10569
var vx []string

loader/validate.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package loader
1818

1919
import (
20-
"context"
2120
"errors"
2221
"fmt"
2322
"strings"
@@ -76,12 +75,6 @@ func checkConsistency(project *types.Project) error {
7675
return fmt.Errorf("service %q depends on undefined service %q: %w", s.Name, dependedService, errdefs.ErrInvalid)
7776
}
7877
}
79-
// Check there isn't a cycle in depends_on declarations
80-
if err := graph.InDependencyOrder(context.Background(), project, func(ctx context.Context, s string, config types.ServiceConfig) error {
81-
return nil
82-
}); err != nil {
83-
return err
84-
}
8578

8679
if strings.HasPrefix(s.NetworkMode, types.ServicePrefix) {
8780
serviceName := s.NetworkMode[len(types.ServicePrefix):]
@@ -159,5 +152,5 @@ func checkConsistency(project *types.Project) error {
159152
}
160153
}
161154

162-
return nil
155+
return graph.CheckCycle(project)
163156
}

0 commit comments

Comments
 (0)