Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 107 additions & 39 deletions pkg/module/generator/ordered_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,53 @@ import (

type resource v1.Resource

// DefaultOrderedKinds provides the default order of Kubernetes resource kinds.
var DefaultOrderedKinds = []string{
"Namespace",
"ResourceQuota",
"StorageClass",
"CustomResourceDefinition",
"ServiceAccount",
"PodSecurityPolicy",
"Role",
"ClusterRole",
"RoleBinding",
"ClusterRoleBinding",
"ConfigMap",
"Secret",
"Endpoints",
"Service",
"LimitRange",
"PriorityClass",
"PersistentVolume",
"PersistentVolumeClaim",
"Deployment",
"StatefulSet",
"CronJob",
"PodDisruptionBudget",
"MutatingWebhookConfiguration",
"ValidatingWebhookConfiguration",
// DefaultDependsKindsGraph defines the default dependency relationships between
// Kubernetes resource kinds. This graph maps each resource kind to the list of
// resource kinds it potentially depends on (not strictly required, but commonly
// associated in practice).
//
// Structure:
// - Key: The resource kind (e.g., "Deployment")
// - Value: Slice of resource kinds this resource may depend on
//
// Example:
//
// "Deployment": {"Namespace", "ServiceAccount", ...}
var DefaultDependsKindsGraph = map[string][]string{
"Namespace": {},
"ResourceQuota": {"Namespace"},
"StorageClass": {},
"CustomResourceDefinition": {},
"ServiceAccount": {"Namespace"},
"PodSecurityPolicy": {},
"Role": {"Namespace"},
"ClusterRole": {},
"RoleBinding": {"Namespace", "ServiceAccount", "Role"},
"ClusterRoleBinding": {"ServiceAccount", "ClusterRole"},
"ConfigMap": {"Namespace"},
"Secret": {"Namespace"},
"Endpoints": {"Namespace"},
"Service": {"Namespace", "Endpoints"},
"LimitRange": {"Namespace", "StorageClass"},
"PriorityClass": {},
"PersistentVolume": {"StorageClass"},
"PersistentVolumeClaim": {"Namespace", "ResourceQuota", "StorageClass", "PersistentVolume"},
"Deployment": {"Namespace", "ResourceQuota", "PersistentVolumeClaim", "ServiceAccount", "PodSecurityPolicy", "ConfigMap", "Secret", "Service", "LimitRange"},
"StatefulSet": {"Namespace", "ResourceQuota", "PersistentVolumeClaim", "ServiceAccount", "PodSecurityPolicy", "ConfigMap", "Secret", "Service", "LimitRange"},
"CronJob": {"Namespace", "ResourceQuota", "PersistentVolumeClaim", "ServiceAccount", "PodSecurityPolicy", "ConfigMap", "Secret", "Service", "LimitRange"},
"PodDisruptionBudget": {"Namespace", "Deployment", "StatefulSet", "CronJob"},
"MutatingWebhookConfiguration": {"Namespace", "ServiceAccount", "RoleBinding", "ClusterRoleBinding", "ConfigMap", "Secret", "Service"},
"ValidatingWebhookConfiguration": {"Namespace", "ServiceAccount", "RoleBinding", "ClusterRoleBinding", "ConfigMap", "Secret", "Service"},
}

// OrderedResources returns a list of Kusion Resources with the injected `dependsOn`
// in a specified order.
func OrderedResources(ctx context.Context, resources v1.Resources, orderedKinds []string) (v1.Resources, error) {
if len(orderedKinds) == 0 {
orderedKinds = DefaultOrderedKinds
func OrderedResources(ctx context.Context, resources v1.Resources, dependsKindsGraph map[string][]string) (v1.Resources, error) {
if dependsKindsGraph == nil {
dependsKindsGraph = DefaultDependsKindsGraph
}
if HasCycleInGraph(dependsKindsGraph) {
return nil, errors.New("find cycles in giving depends kinds grach")
}

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

// Inject dependsOn of the resource.
r := (*resource)(&resources[i])
r.injectDependsOn(orderedKinds, resources)
r.injectDependsOn(dependsKindsGraph, resources)
resources[i] = v1.Resource(*r)
}

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

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

// findDependKinds returns the dependent resource kinds for the specified kind.
func (r *resource) findDependKinds(orderedKinds []string) []string {
func (r *resource) findDependKinds(dependsKindsGraph map[string][]string) []string {
curKind := r.kubernetesKind()
dependKinds := make([]string, 0)
for _, previousKind := range orderedKinds {
if curKind == previousKind {
break
if _, exists := dependsKindsGraph[curKind]; !exists {
depends := []string{}
for resourceKinds, resourceKindsDepends := range dependsKindsGraph {
depends = append(depends, resourceKinds)
for _, resourceKindDepend := range resourceKindsDepends {
// if this curKind is depends by other kinds, and not in this graph,
// return empty depends.
if curKind == resourceKindDepend {
return []string{}
}
}
}
dependKinds = append(dependKinds, previousKind)
// if this curKind is not depends by any other kinds, and not in this graph,
// curkind will depends on all kinds in this graph.
return depends
}
return dependKinds
return dependsKindsGraph[curKind]
}

// findDependResources returns the dependent resources of the specified kind.
Expand All @@ -110,3 +133,48 @@ func findDependResources(dependKind string, rs []v1.Resource) []*v1.Resource {
}
return dependResources
}

// HasCycleInGraph checks if there's a cycle in the dependency graph.
// Returns true if a cycle is detected, false otherwise.
func HasCycleInGraph(graph map[string][]string) bool {
// Track visited nodes and recursion stack for cycle detection
visited := make(map[string]bool)
recursionStack := make(map[string]bool)

// Check each node in the graph
for node := range graph {
if !visited[node] {
if hasCycle(node, visited, recursionStack, graph) {
return true // Cycle detected
}
}
}

return false // No cycle found
}

// hasCycle performs DFS to detect cycles recursively
func hasCycle(node string, visited, recursionStack map[string]bool, graph map[string][]string) bool {
if recursionStack[node] {
return true // Cycle detected
}

if visited[node] {
return false // Already visited and no cycle found
}

// Mark as visited and add to recursion stack
visited[node] = true
recursionStack[node] = true

// Recursively check dependencies
for _, dep := range graph[node] {
if hasCycle(dep, visited, recursionStack, graph) {
return true
}
}

// Remove from recursion stack after processing
recursionStack[node] = false
return false
}
76 changes: 42 additions & 34 deletions pkg/module/generator/ordered_resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,25 +67,25 @@ var (
}
)

func TestOrderedResources(t *testing.T) {
func TestDependsKindsGraphResources(t *testing.T) {
tests := []struct {
name string
resources v1.Resources
orderedKinds []string
resExpected v1.Resources
errExpected bool
name string
resources v1.Resources
dependsKindsGraph map[string][]string
resExpected v1.Resources
errExpected bool
}{
{
name: "empty resources",
resources: v1.Resources{},
orderedKinds: nil,
resExpected: nil,
errExpected: true,
name: "empty resources",
resources: v1.Resources{},
dependsKindsGraph: nil,
resExpected: nil,
errExpected: true,
},
{
name: "resources with default order",
resources: *genOldResources(),
orderedKinds: nil,
name: "resources with default graph",
resources: *genOldResources(),
dependsKindsGraph: nil,
resExpected: v1.Resources{
{
ID: "apps/v1:Deployment:foo:bar",
Expand Down Expand Up @@ -113,12 +113,11 @@ func TestOrderedResources(t *testing.T) {
errExpected: false,
},
{
name: "resources with specified order",
name: "resources with specified graph",
resources: *genOldResources(),
orderedKinds: []string{
"Deployment",
"Service",
"Namespace",
dependsKindsGraph: map[string][]string{
"Service": {"Deployment"},
"Namespace": {"Service", "Deployment"},
},
resExpected: v1.Resources{
{
Expand All @@ -139,8 +138,8 @@ func TestOrderedResources(t *testing.T) {
Type: v1.Kubernetes,
Attributes: fakeNamespace,
DependsOn: []string{
"apps/v1:Deployment:foo:bar",
"v1:Service:foo:bar",
"apps/v1:Deployment:foo:bar",
},
},
},
Expand All @@ -150,7 +149,7 @@ func TestOrderedResources(t *testing.T) {

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

func TestInjectAllDependsOn(t *testing.T) {
resources := genOldResources()
dependKinds := []string{"Namespace"}
dependsKindsGraph := map[string][]string{
"Namespace": {},
}

expected := []string{"v1:Namespace:foo"}
actual := resource((*resources)[0])
actual.injectDependsOn(dependKinds, *resources)
actual.injectDependsOn(dependsKindsGraph, *resources)

assert.Equal(t, expected, actual.DependsOn)
}
Expand All @@ -195,24 +196,15 @@ func TestFindDependKinds(t *testing.T) {
expected := []string{
"Namespace",
"ResourceQuota",
"StorageClass",
"CustomResourceDefinition",
"PersistentVolumeClaim",
"ServiceAccount",
"PodSecurityPolicy",
"Role",
"ClusterRole",
"RoleBinding",
"ClusterRoleBinding",
"ConfigMap",
"Secret",
"Endpoints",
"Service",
"LimitRange",
"PriorityClass",
"PersistentVolume",
"PersistentVolumeClaim",
}
actual := r.findDependKinds(DefaultOrderedKinds)
actual := r.findDependKinds(DefaultDependsKindsGraph)

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

assert.Equal(t, expected, actual)
}

func TestDefaultDependsGraphValidation(t *testing.T) {
for resourceKind, dependResourceKinds := range DefaultDependsKindsGraph {
for _, dependsResourceKind := range dependResourceKinds {
if _, exists := DefaultDependsKindsGraph[dependsResourceKind]; !exists {
t.Errorf("resource type %q depends on a non-existent resource type %q", resourceKind, dependsResourceKind)
}
}
}
}

func TestNoCyclesInDefaultDependencyGraph(t *testing.T) {
if HasCycleInGraph(DefaultDependsKindsGraph) {
t.Errorf("Cycle detected in the dependency graph!")
}
}