Skip to content

Commit b949fc8

Browse files
committed
introduce general-purpose service dependency graph traversal functions
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent 2a3ce93 commit b949fc8

File tree

11 files changed

+1040
-60
lines changed

11 files changed

+1040
-60
lines changed

dotenv/godotenv_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func parseAndCompare(t *testing.T, rawEnvLine string, expectedKey string, expect
1919
assert.NilError(t, err)
2020
actualValue, ok := env[expectedKey]
2121
if !ok {
22-
t.Errorf("Key %q was not found in env: %v", expectedKey, env)
22+
t.Errorf("key %q was not found in env: %v", expectedKey, env)
2323
} else if actualValue != expectedValue {
2424
t.Errorf("Expected '%v' to parse as '%v' => '%v', got '%v' => '%v' instead", rawEnvLine, expectedKey, expectedValue, expectedKey, actualValue)
2525
}

graph/graph.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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+
// graph represents project as service dependencies
29+
type graph struct {
30+
vertices map[string]*vertex
31+
}
32+
33+
// vertex represents a service in the dependencies structure
34+
type vertex struct {
35+
key string
36+
service *types.ServiceConfig
37+
children map[string]*vertex
38+
parents map[string]*vertex
39+
}
40+
41+
// newGraph creates a service graph from project
42+
func newGraph(project *types.Project) (*graph, error) {
43+
g := &graph{
44+
vertices: map[string]*vertex{},
45+
}
46+
47+
for name, s := range project.Services {
48+
g.addVertex(name, s)
49+
}
50+
51+
for name, s := range project.Services {
52+
src := g.vertices[name]
53+
for dep, condition := range s.DependsOn {
54+
dest, ok := g.vertices[dep]
55+
if !ok {
56+
if condition.Required {
57+
if ds, exists := project.DisabledServices[dep]; exists {
58+
return nil, fmt.Errorf("service %q is required by %q but is disabled. Can be enabled by profiles %s", dep, name, ds.Profiles)
59+
}
60+
return nil, fmt.Errorf("service %q depends on unknown service %q", name, dep)
61+
}
62+
delete(s.DependsOn, name)
63+
project.Services[name] = s
64+
continue
65+
}
66+
src.children[dep] = dest
67+
dest.parents[name] = src
68+
}
69+
}
70+
71+
err := g.checkCycle()
72+
return g, err
73+
}
74+
75+
func (g *graph) addVertex(name string, service types.ServiceConfig) {
76+
g.vertices[name] = &vertex{
77+
key: name,
78+
service: &service,
79+
parents: map[string]*vertex{},
80+
children: map[string]*vertex{},
81+
}
82+
}
83+
84+
func (g *graph) addEdge(src, dest string) {
85+
g.vertices[src].children[dest] = g.vertices[dest]
86+
g.vertices[dest].parents[src] = g.vertices[src]
87+
}
88+
89+
func (g *graph) roots() []*vertex {
90+
var res []*vertex
91+
for _, v := range g.vertices {
92+
if len(v.parents) == 0 {
93+
res = append(res, v)
94+
}
95+
}
96+
return res
97+
}
98+
99+
func (g *graph) leaves() []*vertex {
100+
var res []*vertex
101+
for _, v := range g.vertices {
102+
if len(v.children) == 0 {
103+
res = append(res, v)
104+
}
105+
}
106+
107+
return res
108+
}
109+
110+
func (g *graph) checkCycle() error {
111+
// iterate on verticles in a name-order to render a predicable error message
112+
// this is required by tests and enforce command reproducibility by user, which otherwise could be confusing
113+
names := utils.MapKeys(g.vertices)
114+
for _, name := range names {
115+
err := searchCycle([]string{name}, g.vertices[name])
116+
if err != nil {
117+
return err
118+
}
119+
}
120+
return nil
121+
}
122+
123+
func searchCycle(path []string, v *vertex) error {
124+
names := utils.MapKeys(v.children)
125+
for _, name := range names {
126+
if i := slices.Index(path, name); i > 0 {
127+
return fmt.Errorf("dependency cycle detected: %s", strings.Join(path[i:], " -> "))
128+
}
129+
ch := v.children[name]
130+
err := searchCycle(append(path, name), ch)
131+
if err != nil {
132+
return err
133+
}
134+
}
135+
return nil
136+
}
137+
138+
// descendents return all descendents for a vertex, might contain duplicates
139+
func (v *vertex) descendents() []string {
140+
var vx []string
141+
for _, n := range v.children {
142+
vx = append(vx, n.key)
143+
vx = append(vx, n.descendents()...)
144+
}
145+
return vx
146+
}

0 commit comments

Comments
 (0)