Skip to content

Commit 6cfe2ff

Browse files
authored
Merge pull request #32 from fluxcd/dep-sort
Sort dependencies with Tarjan's SCCS algorithm
2 parents 89ac8fb + fdbf69c commit 6cfe2ff

File tree

3 files changed

+281
-4
lines changed

3 files changed

+281
-4
lines changed

api/v1alpha1/sort.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
Copyright 2020 The Flux CD contributors.
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 v1alpha1
18+
19+
import (
20+
"fmt"
21+
)
22+
23+
// +kubebuilder:object:generate=false
24+
type Unsortable [][]string
25+
26+
func (e Unsortable) Error() string {
27+
return fmt.Sprintf("circular dependencies: %v", [][]string(e))
28+
}
29+
30+
func DependencySort(ks []Kustomization) ([]Kustomization, error) {
31+
n := make(graph)
32+
lookup := map[string]*Kustomization{}
33+
for i := 0; i < len(ks); i++ {
34+
n[ks[i].Name] = after(ks[i].Spec.DependsOn)
35+
lookup[ks[i].Name] = &ks[i]
36+
}
37+
sccs := tarjanSCC(n)
38+
var sorted []Kustomization
39+
var unsortable Unsortable
40+
for i := 0; i < len(sccs); i++ {
41+
s := sccs[i]
42+
if len(s) != 1 {
43+
unsortable = append(unsortable, s)
44+
continue
45+
}
46+
if k, ok := lookup[s[0]]; ok {
47+
sorted = append(sorted, *k.DeepCopy())
48+
}
49+
}
50+
if unsortable != nil {
51+
for i, j := 0, len(unsortable)-1; i < j; i, j = i+1, j-1 {
52+
unsortable[i], unsortable[j] = unsortable[j], unsortable[i]
53+
}
54+
return nil, unsortable
55+
}
56+
return sorted, nil
57+
}
58+
59+
// https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm#The_algorithm_in_pseudocode
60+
61+
type graph map[string]edges
62+
63+
type edges map[string]struct{}
64+
65+
func after(i []string) edges {
66+
if len(i) == 0 {
67+
return nil
68+
}
69+
s := make(edges)
70+
for _, v := range i {
71+
s[v] = struct{}{}
72+
}
73+
return s
74+
}
75+
76+
func tarjanSCC(g graph) [][]string {
77+
t := tarjan{
78+
g: g,
79+
80+
indexTable: make(map[string]int, len(g)),
81+
lowLink: make(map[string]int, len(g)),
82+
onStack: make(map[string]bool, len(g)),
83+
}
84+
for v := range t.g {
85+
if t.indexTable[v] == 0 {
86+
t.strongconnect(v)
87+
}
88+
}
89+
return t.sccs
90+
}
91+
92+
type tarjan struct {
93+
g graph
94+
95+
index int
96+
indexTable map[string]int
97+
lowLink map[string]int
98+
onStack map[string]bool
99+
100+
stack []string
101+
102+
sccs [][]string
103+
}
104+
105+
func (t *tarjan) strongconnect(v string) {
106+
// Set the depth index for v to the smallest unused index.
107+
t.index++
108+
t.indexTable[v] = t.index
109+
t.lowLink[v] = t.index
110+
t.stack = append(t.stack, v)
111+
t.onStack[v] = true
112+
113+
// Consider successors of v.
114+
for w := range t.g[v] {
115+
if t.indexTable[w] == 0 {
116+
// Successor w has not yet been visited; recur on it.
117+
t.strongconnect(w)
118+
t.lowLink[v] = min(t.lowLink[v], t.lowLink[w])
119+
} else if t.onStack[w] {
120+
// Successor w is in stack s and hence in the current SCC.
121+
t.lowLink[v] = min(t.lowLink[v], t.indexTable[w])
122+
}
123+
}
124+
125+
// If v is a root graph, pop the stack and generate an SCC.
126+
if t.lowLink[v] == t.indexTable[v] {
127+
// Start a new strongly connected component.
128+
var (
129+
scc []string
130+
w string
131+
)
132+
for {
133+
w, t.stack = t.stack[len(t.stack)-1], t.stack[:len(t.stack)-1]
134+
t.onStack[w] = false
135+
// Add w to current strongly connected component.
136+
scc = append(scc, w)
137+
if w == v {
138+
break
139+
}
140+
}
141+
// Output the current strongly connected component.
142+
t.sccs = append(t.sccs, scc)
143+
}
144+
}
145+
146+
func min(a, b int) int {
147+
if a < b {
148+
return a
149+
}
150+
return b
151+
}

api/v1alpha1/sort_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
Copyright 2020 The Flux CD contributors.
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 v1alpha1
18+
19+
import (
20+
"reflect"
21+
"testing"
22+
23+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
)
25+
26+
func TestDependencySort(t *testing.T) {
27+
tests := []struct {
28+
name string
29+
ks []Kustomization
30+
want []Kustomization
31+
wantErr bool
32+
}{
33+
{
34+
"simple",
35+
[]Kustomization{
36+
{
37+
ObjectMeta: v1.ObjectMeta{Name: "frontend"},
38+
Spec: KustomizationSpec{DependsOn: []string{"backend"}},
39+
},
40+
{
41+
ObjectMeta: v1.ObjectMeta{Name: "common"},
42+
},
43+
{
44+
ObjectMeta: v1.ObjectMeta{Name: "backend"},
45+
Spec: KustomizationSpec{DependsOn: []string{"common"}},
46+
},
47+
},
48+
[]Kustomization{
49+
{
50+
ObjectMeta: v1.ObjectMeta{Name: "common"},
51+
},
52+
{
53+
ObjectMeta: v1.ObjectMeta{Name: "backend"},
54+
Spec: KustomizationSpec{DependsOn: []string{"common"}},
55+
},
56+
{
57+
ObjectMeta: v1.ObjectMeta{Name: "frontend"},
58+
Spec: KustomizationSpec{DependsOn: []string{"backend"}},
59+
},
60+
},
61+
false,
62+
},
63+
{
64+
"circle dependency",
65+
[]Kustomization{
66+
{
67+
ObjectMeta: v1.ObjectMeta{Name: "dependency"},
68+
Spec: KustomizationSpec{DependsOn: []string{"endless"}},
69+
},
70+
{
71+
ObjectMeta: v1.ObjectMeta{Name: "endless"},
72+
Spec: KustomizationSpec{DependsOn: []string{"circular"}},
73+
},
74+
{
75+
ObjectMeta: v1.ObjectMeta{Name: "circular"},
76+
Spec: KustomizationSpec{DependsOn: []string{"dependency"}},
77+
},
78+
},
79+
nil,
80+
true,
81+
},
82+
}
83+
for _, tt := range tests {
84+
t.Run(tt.name, func(t *testing.T) {
85+
got, err := DependencySort(tt.ks)
86+
if (err != nil) != tt.wantErr {
87+
t.Errorf("DependencySort() error = %v, wantErr %v", err, tt.wantErr)
88+
return
89+
}
90+
if !reflect.DeepEqual(got, tt.want) {
91+
t.Errorf("DependencySort() got = %v, want %v", got, tt.want)
92+
}
93+
})
94+
}
95+
}
96+
97+
func TestDependencySort_DeadEnd(t *testing.T) {
98+
ks := []Kustomization{
99+
{
100+
ObjectMeta: v1.ObjectMeta{Name: "backend"},
101+
Spec: KustomizationSpec{DependsOn: []string{"common"}},
102+
},
103+
{
104+
ObjectMeta: v1.ObjectMeta{Name: "frontend"},
105+
Spec: KustomizationSpec{DependsOn: []string{"infra"}},
106+
},
107+
{
108+
ObjectMeta: v1.ObjectMeta{Name: "common"},
109+
},
110+
}
111+
got, err := DependencySort(ks)
112+
if err != nil {
113+
t.Errorf("DependencySort() error = %v", err)
114+
return
115+
}
116+
if len(got) != len(ks) {
117+
t.Errorf("DependencySort() len = %v, want %v", len(got), len(ks))
118+
}
119+
}

controllers/gitrepository_controller.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ import (
2929
ctrl "sigs.k8s.io/controller-runtime"
3030
"sigs.k8s.io/controller-runtime/pkg/client"
3131

32-
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1"
3332
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
33+
34+
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1"
3435
)
3536

3637
// KustomizationReconciler watches a GitRepository object
@@ -63,10 +64,16 @@ func (r *GitRepositoryWatcher) Reconcile(req ctrl.Request) (ctrl.Result, error)
6364
return ctrl.Result{}, err
6465
}
6566

67+
sorted, err := kustomizev1.DependencySort(list.Items)
68+
if err != nil {
69+
log.Error(err, "unable to dependency sort kustomizations")
70+
return ctrl.Result{}, err
71+
}
72+
6673
// trigger apply for each kustomization using this Git repository
67-
for _, kustomization := range list.Items {
68-
namespacedName := types.NamespacedName{Namespace: kustomization.Namespace, Name: kustomization.Name}
69-
if err := r.requestKustomizationSync(kustomization); err != nil {
74+
for _, k := range sorted {
75+
namespacedName := types.NamespacedName{Namespace: k.Namespace, Name: k.Name}
76+
if err := r.requestKustomizationSync(k); err != nil {
7077
log.Error(err, "unable to annotate Kustomization", "kustomization", namespacedName)
7178
continue
7279
}

0 commit comments

Comments
 (0)