From a98f39948a157aba79d46656b2cbc5f3ea78d505 Mon Sep 17 00:00:00 2001 From: ruquanzhao Date: Wed, 18 Jun 2025 16:05:13 +0800 Subject: [PATCH] feat: use graph based resource kinds depends --- pkg/module/generator/ordered_resources.go | 146 +++++++++++++----- .../generator/ordered_resources_test.go | 76 +++++---- 2 files changed, 149 insertions(+), 73 deletions(-) diff --git a/pkg/module/generator/ordered_resources.go b/pkg/module/generator/ordered_resources.go index f7252cf..2987a59 100644 --- a/pkg/module/generator/ordered_resources.go +++ b/pkg/module/generator/ordered_resources.go @@ -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 { @@ -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) } @@ -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) @@ -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. @@ -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 +} diff --git a/pkg/module/generator/ordered_resources_test.go b/pkg/module/generator/ordered_resources_test.go index 35381f7..4c969c1 100644 --- a/pkg/module/generator/ordered_resources_test.go +++ b/pkg/module/generator/ordered_resources_test.go @@ -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", @@ -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{ { @@ -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", }, }, }, @@ -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) } @@ -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) } @@ -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) } @@ -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!") + } +}