Skip to content

Commit a98f399

Browse files
committed
feat: use graph based resource kinds depends
1 parent 5391576 commit a98f399

File tree

2 files changed

+149
-73
lines changed

2 files changed

+149
-73
lines changed

pkg/module/generator/ordered_resources.go

Lines changed: 107 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,53 @@ import (
1010

1111
type resource v1.Resource
1212

13-
// DefaultOrderedKinds provides the default order of Kubernetes resource kinds.
14-
var DefaultOrderedKinds = []string{
15-
"Namespace",
16-
"ResourceQuota",
17-
"StorageClass",
18-
"CustomResourceDefinition",
19-
"ServiceAccount",
20-
"PodSecurityPolicy",
21-
"Role",
22-
"ClusterRole",
23-
"RoleBinding",
24-
"ClusterRoleBinding",
25-
"ConfigMap",
26-
"Secret",
27-
"Endpoints",
28-
"Service",
29-
"LimitRange",
30-
"PriorityClass",
31-
"PersistentVolume",
32-
"PersistentVolumeClaim",
33-
"Deployment",
34-
"StatefulSet",
35-
"CronJob",
36-
"PodDisruptionBudget",
37-
"MutatingWebhookConfiguration",
38-
"ValidatingWebhookConfiguration",
13+
// DefaultDependsKindsGraph defines the default dependency relationships between
14+
// Kubernetes resource kinds. This graph maps each resource kind to the list of
15+
// resource kinds it potentially depends on (not strictly required, but commonly
16+
// associated in practice).
17+
//
18+
// Structure:
19+
// - Key: The resource kind (e.g., "Deployment")
20+
// - Value: Slice of resource kinds this resource may depend on
21+
//
22+
// Example:
23+
//
24+
// "Deployment": {"Namespace", "ServiceAccount", ...}
25+
var DefaultDependsKindsGraph = map[string][]string{
26+
"Namespace": {},
27+
"ResourceQuota": {"Namespace"},
28+
"StorageClass": {},
29+
"CustomResourceDefinition": {},
30+
"ServiceAccount": {"Namespace"},
31+
"PodSecurityPolicy": {},
32+
"Role": {"Namespace"},
33+
"ClusterRole": {},
34+
"RoleBinding": {"Namespace", "ServiceAccount", "Role"},
35+
"ClusterRoleBinding": {"ServiceAccount", "ClusterRole"},
36+
"ConfigMap": {"Namespace"},
37+
"Secret": {"Namespace"},
38+
"Endpoints": {"Namespace"},
39+
"Service": {"Namespace", "Endpoints"},
40+
"LimitRange": {"Namespace", "StorageClass"},
41+
"PriorityClass": {},
42+
"PersistentVolume": {"StorageClass"},
43+
"PersistentVolumeClaim": {"Namespace", "ResourceQuota", "StorageClass", "PersistentVolume"},
44+
"Deployment": {"Namespace", "ResourceQuota", "PersistentVolumeClaim", "ServiceAccount", "PodSecurityPolicy", "ConfigMap", "Secret", "Service", "LimitRange"},
45+
"StatefulSet": {"Namespace", "ResourceQuota", "PersistentVolumeClaim", "ServiceAccount", "PodSecurityPolicy", "ConfigMap", "Secret", "Service", "LimitRange"},
46+
"CronJob": {"Namespace", "ResourceQuota", "PersistentVolumeClaim", "ServiceAccount", "PodSecurityPolicy", "ConfigMap", "Secret", "Service", "LimitRange"},
47+
"PodDisruptionBudget": {"Namespace", "Deployment", "StatefulSet", "CronJob"},
48+
"MutatingWebhookConfiguration": {"Namespace", "ServiceAccount", "RoleBinding", "ClusterRoleBinding", "ConfigMap", "Secret", "Service"},
49+
"ValidatingWebhookConfiguration": {"Namespace", "ServiceAccount", "RoleBinding", "ClusterRoleBinding", "ConfigMap", "Secret", "Service"},
3950
}
4051

4152
// OrderedResources returns a list of Kusion Resources with the injected `dependsOn`
4253
// in a specified order.
43-
func OrderedResources(ctx context.Context, resources v1.Resources, orderedKinds []string) (v1.Resources, error) {
44-
if len(orderedKinds) == 0 {
45-
orderedKinds = DefaultOrderedKinds
54+
func OrderedResources(ctx context.Context, resources v1.Resources, dependsKindsGraph map[string][]string) (v1.Resources, error) {
55+
if dependsKindsGraph == nil {
56+
dependsKindsGraph = DefaultDependsKindsGraph
57+
}
58+
if HasCycleInGraph(dependsKindsGraph) {
59+
return nil, errors.New("find cycles in giving depends kinds grach")
4660
}
4761

4862
if len(resources) == 0 {
@@ -57,7 +71,7 @@ func OrderedResources(ctx context.Context, resources v1.Resources, orderedKinds
5771

5872
// Inject dependsOn of the resource.
5973
r := (*resource)(&resources[i])
60-
r.injectDependsOn(orderedKinds, resources)
74+
r.injectDependsOn(dependsKindsGraph, resources)
6175
resources[i] = v1.Resource(*r)
6276
}
6377

@@ -72,8 +86,8 @@ func (r resource) kubernetesKind() string {
7286
}
7387

7488
// injectDependsOn injects all dependsOn relationships for the given resource and dependent kinds.
75-
func (r *resource) injectDependsOn(orderedKinds []string, rs []v1.Resource) {
76-
kinds := r.findDependKinds(orderedKinds)
89+
func (r *resource) injectDependsOn(dependsKindsGraph map[string][]string, rs []v1.Resource) {
90+
kinds := r.findDependKinds(dependsKindsGraph)
7791
for _, kind := range kinds {
7892
drs := findDependResources(kind, rs)
7993
r.appendDependsOn(drs)
@@ -88,16 +102,25 @@ func (r *resource) appendDependsOn(dependResources []*v1.Resource) {
88102
}
89103

90104
// findDependKinds returns the dependent resource kinds for the specified kind.
91-
func (r *resource) findDependKinds(orderedKinds []string) []string {
105+
func (r *resource) findDependKinds(dependsKindsGraph map[string][]string) []string {
92106
curKind := r.kubernetesKind()
93-
dependKinds := make([]string, 0)
94-
for _, previousKind := range orderedKinds {
95-
if curKind == previousKind {
96-
break
107+
if _, exists := dependsKindsGraph[curKind]; !exists {
108+
depends := []string{}
109+
for resourceKinds, resourceKindsDepends := range dependsKindsGraph {
110+
depends = append(depends, resourceKinds)
111+
for _, resourceKindDepend := range resourceKindsDepends {
112+
// if this curKind is depends by other kinds, and not in this graph,
113+
// return empty depends.
114+
if curKind == resourceKindDepend {
115+
return []string{}
116+
}
117+
}
97118
}
98-
dependKinds = append(dependKinds, previousKind)
119+
// if this curKind is not depends by any other kinds, and not in this graph,
120+
// curkind will depends on all kinds in this graph.
121+
return depends
99122
}
100-
return dependKinds
123+
return dependsKindsGraph[curKind]
101124
}
102125

103126
// findDependResources returns the dependent resources of the specified kind.
@@ -110,3 +133,48 @@ func findDependResources(dependKind string, rs []v1.Resource) []*v1.Resource {
110133
}
111134
return dependResources
112135
}
136+
137+
// HasCycleInGraph checks if there's a cycle in the dependency graph.
138+
// Returns true if a cycle is detected, false otherwise.
139+
func HasCycleInGraph(graph map[string][]string) bool {
140+
// Track visited nodes and recursion stack for cycle detection
141+
visited := make(map[string]bool)
142+
recursionStack := make(map[string]bool)
143+
144+
// Check each node in the graph
145+
for node := range graph {
146+
if !visited[node] {
147+
if hasCycle(node, visited, recursionStack, graph) {
148+
return true // Cycle detected
149+
}
150+
}
151+
}
152+
153+
return false // No cycle found
154+
}
155+
156+
// hasCycle performs DFS to detect cycles recursively
157+
func hasCycle(node string, visited, recursionStack map[string]bool, graph map[string][]string) bool {
158+
if recursionStack[node] {
159+
return true // Cycle detected
160+
}
161+
162+
if visited[node] {
163+
return false // Already visited and no cycle found
164+
}
165+
166+
// Mark as visited and add to recursion stack
167+
visited[node] = true
168+
recursionStack[node] = true
169+
170+
// Recursively check dependencies
171+
for _, dep := range graph[node] {
172+
if hasCycle(dep, visited, recursionStack, graph) {
173+
return true
174+
}
175+
}
176+
177+
// Remove from recursion stack after processing
178+
recursionStack[node] = false
179+
return false
180+
}

pkg/module/generator/ordered_resources_test.go

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -67,25 +67,25 @@ var (
6767
}
6868
)
6969

70-
func TestOrderedResources(t *testing.T) {
70+
func TestDependsKindsGraphResources(t *testing.T) {
7171
tests := []struct {
72-
name string
73-
resources v1.Resources
74-
orderedKinds []string
75-
resExpected v1.Resources
76-
errExpected bool
72+
name string
73+
resources v1.Resources
74+
dependsKindsGraph map[string][]string
75+
resExpected v1.Resources
76+
errExpected bool
7777
}{
7878
{
79-
name: "empty resources",
80-
resources: v1.Resources{},
81-
orderedKinds: nil,
82-
resExpected: nil,
83-
errExpected: true,
79+
name: "empty resources",
80+
resources: v1.Resources{},
81+
dependsKindsGraph: nil,
82+
resExpected: nil,
83+
errExpected: true,
8484
},
8585
{
86-
name: "resources with default order",
87-
resources: *genOldResources(),
88-
orderedKinds: nil,
86+
name: "resources with default graph",
87+
resources: *genOldResources(),
88+
dependsKindsGraph: nil,
8989
resExpected: v1.Resources{
9090
{
9191
ID: "apps/v1:Deployment:foo:bar",
@@ -113,12 +113,11 @@ func TestOrderedResources(t *testing.T) {
113113
errExpected: false,
114114
},
115115
{
116-
name: "resources with specified order",
116+
name: "resources with specified graph",
117117
resources: *genOldResources(),
118-
orderedKinds: []string{
119-
"Deployment",
120-
"Service",
121-
"Namespace",
118+
dependsKindsGraph: map[string][]string{
119+
"Service": {"Deployment"},
120+
"Namespace": {"Service", "Deployment"},
122121
},
123122
resExpected: v1.Resources{
124123
{
@@ -139,8 +138,8 @@ func TestOrderedResources(t *testing.T) {
139138
Type: v1.Kubernetes,
140139
Attributes: fakeNamespace,
141140
DependsOn: []string{
142-
"apps/v1:Deployment:foo:bar",
143141
"v1:Service:foo:bar",
142+
"apps/v1:Deployment:foo:bar",
144143
},
145144
},
146145
},
@@ -150,7 +149,7 @@ func TestOrderedResources(t *testing.T) {
150149

151150
for _, tt := range tests {
152151
t.Run(tt.name, func(t *testing.T) {
153-
got, err := OrderedResources(context.Background(), tt.resources, tt.orderedKinds)
152+
got, err := OrderedResources(context.Background(), tt.resources, tt.dependsKindsGraph)
154153
if (err != nil) != tt.errExpected {
155154
t.Errorf("OrderedResources() error = %v, errWanted = %v", err, tt.errExpected)
156155
}
@@ -175,11 +174,13 @@ func TestResourceKind(t *testing.T) {
175174

176175
func TestInjectAllDependsOn(t *testing.T) {
177176
resources := genOldResources()
178-
dependKinds := []string{"Namespace"}
177+
dependsKindsGraph := map[string][]string{
178+
"Namespace": {},
179+
}
179180

180181
expected := []string{"v1:Namespace:foo"}
181182
actual := resource((*resources)[0])
182-
actual.injectDependsOn(dependKinds, *resources)
183+
actual.injectDependsOn(dependsKindsGraph, *resources)
183184

184185
assert.Equal(t, expected, actual.DependsOn)
185186
}
@@ -195,24 +196,15 @@ func TestFindDependKinds(t *testing.T) {
195196
expected := []string{
196197
"Namespace",
197198
"ResourceQuota",
198-
"StorageClass",
199-
"CustomResourceDefinition",
199+
"PersistentVolumeClaim",
200200
"ServiceAccount",
201201
"PodSecurityPolicy",
202-
"Role",
203-
"ClusterRole",
204-
"RoleBinding",
205-
"ClusterRoleBinding",
206202
"ConfigMap",
207203
"Secret",
208-
"Endpoints",
209204
"Service",
210205
"LimitRange",
211-
"PriorityClass",
212-
"PersistentVolume",
213-
"PersistentVolumeClaim",
214206
}
215-
actual := r.findDependKinds(DefaultOrderedKinds)
207+
actual := r.findDependKinds(DefaultDependsKindsGraph)
216208

217209
assert.Equal(t, expected, actual)
218210
}
@@ -232,3 +224,19 @@ func TestFindDependResources(t *testing.T) {
232224

233225
assert.Equal(t, expected, actual)
234226
}
227+
228+
func TestDefaultDependsGraphValidation(t *testing.T) {
229+
for resourceKind, dependResourceKinds := range DefaultDependsKindsGraph {
230+
for _, dependsResourceKind := range dependResourceKinds {
231+
if _, exists := DefaultDependsKindsGraph[dependsResourceKind]; !exists {
232+
t.Errorf("resource type %q depends on a non-existent resource type %q", resourceKind, dependsResourceKind)
233+
}
234+
}
235+
}
236+
}
237+
238+
func TestNoCyclesInDefaultDependencyGraph(t *testing.T) {
239+
if HasCycleInGraph(DefaultDependsKindsGraph) {
240+
t.Errorf("Cycle detected in the dependency graph!")
241+
}
242+
}

0 commit comments

Comments
 (0)