mirror of
https://github.com/kubesphere/kubekey.git
synced 2025-12-25 17:12:50 +00:00
feature: abandan file cycle import (#2721)
* feature: abandan file cycle import Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feature: abandan file cycle import Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feature: abandan file cycle import Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feature: abandan file cycle import Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feature: abandan file cycle import Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feature: abandan file cycle import Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> * fix: add comment Signed-off-by: redscholar <blacktiledhouse@gmail.com> --------- Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> Signed-off-by: redscholar <blacktiledhouse@gmail.com> Co-authored-by: redscholar <blacktiledhouse@gmail.com>
This commit is contained in:
parent
ba98704f30
commit
9711164ff7
|
|
@ -31,6 +31,7 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
|
||||||
_const "github.com/kubesphere/kubekey/v4/pkg/const"
|
_const "github.com/kubesphere/kubekey/v4/pkg/const"
|
||||||
|
"github.com/kubesphere/kubekey/v4/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newGitProject(ctx context.Context, playbook kkcorev1.Playbook, update bool) (Project, error) {
|
func newGitProject(ctx context.Context, playbook kkcorev1.Playbook, update bool) (Project, error) {
|
||||||
|
|
@ -65,10 +66,11 @@ func newGitProject(ctx context.Context, playbook kkcorev1.Playbook, update bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &project{
|
return &project{
|
||||||
FS: os.DirFS(filepath.Join(projectDir, playbook.Spec.Project.Name)),
|
FS: os.DirFS(filepath.Join(projectDir, playbook.Spec.Project.Name)),
|
||||||
basePlaybook: playbook.Spec.Playbook,
|
basePlaybook: playbook.Spec.Playbook,
|
||||||
Playbook: &kkprojectv1.Playbook{},
|
Playbook: &kkprojectv1.Playbook{},
|
||||||
config: playbook.Spec.Config.Value(),
|
config: playbook.Spec.Config.Value(),
|
||||||
|
playbookGraph: utils.NewKahnGraph(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import (
|
||||||
"github.com/cockroachdb/errors"
|
"github.com/cockroachdb/errors"
|
||||||
kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
|
kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
|
||||||
kkprojectv1 "github.com/kubesphere/kubekey/api/project/v1"
|
kkprojectv1 "github.com/kubesphere/kubekey/api/project/v1"
|
||||||
|
|
||||||
|
"github.com/kubesphere/kubekey/v4/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newLocalProject(playbook kkcorev1.Playbook) (Project, error) {
|
func newLocalProject(playbook kkcorev1.Playbook) (Project, error) {
|
||||||
|
|
@ -47,9 +49,10 @@ func newLocalProject(playbook kkcorev1.Playbook) (Project, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &project{
|
return &project{
|
||||||
FS: os.DirFS(projectPath),
|
FS: os.DirFS(projectPath),
|
||||||
basePlaybook: relPath,
|
basePlaybook: relPath,
|
||||||
Playbook: &kkprojectv1.Playbook{},
|
Playbook: &kkprojectv1.Playbook{},
|
||||||
config: playbook.Spec.Config.Value(),
|
config: playbook.Spec.Config.Value(),
|
||||||
|
playbookGraph: utils.NewKahnGraph(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,8 @@ type project struct {
|
||||||
basePlaybook string
|
basePlaybook string
|
||||||
*kkprojectv1.Playbook
|
*kkprojectv1.Playbook
|
||||||
|
|
||||||
config map[string]any
|
config map[string]any
|
||||||
|
playbookGraph *utils.KahnGraph
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadFile reads and returns the contents of the file at the given path
|
// ReadFile reads and returns the contents of the file at the given path
|
||||||
|
|
@ -105,7 +106,7 @@ func (f *project) WalkDir(path string, fn fs.WalkDirFunc) error {
|
||||||
func (f *project) MarshalPlaybook() (*kkprojectv1.Playbook, error) {
|
func (f *project) MarshalPlaybook() (*kkprojectv1.Playbook, error) {
|
||||||
f.Playbook = &kkprojectv1.Playbook{}
|
f.Playbook = &kkprojectv1.Playbook{}
|
||||||
// convert playbook to kkprojectv1.Playbook
|
// convert playbook to kkprojectv1.Playbook
|
||||||
if err := f.loadPlaybook(f.basePlaybook); err != nil {
|
if err := f.loadPlaybook("", f.basePlaybook); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// validate playbook
|
// validate playbook
|
||||||
|
|
@ -117,8 +118,13 @@ func (f *project) MarshalPlaybook() (*kkprojectv1.Playbook, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadPlaybook loads a playbook and all its included playbooks into a single playbook
|
// loadPlaybook loads a playbook and all its included playbooks into a single playbook
|
||||||
func (f *project) loadPlaybook(basePlaybook string) error {
|
func (f *project) loadPlaybook(fromPlayBook, basePlaybook string) error {
|
||||||
// baseDir is the local ansible project dir which playbook belong to
|
// baseDir is the local ansible project dir which playbook belong to
|
||||||
|
if f.playbookGraph.AddEdgeAndCheckCycle(fromPlayBook, basePlaybook) {
|
||||||
|
// play book cycle imported
|
||||||
|
return errors.Errorf("failed to import %s because it cause a cycle import", basePlaybook)
|
||||||
|
}
|
||||||
|
|
||||||
pbData, err := fs.ReadFile(f.FS, basePlaybook)
|
pbData, err := fs.ReadFile(f.FS, basePlaybook)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "failed to read playbook %q", basePlaybook)
|
return errors.Wrapf(err, "failed to read playbook %q", basePlaybook)
|
||||||
|
|
@ -173,7 +179,11 @@ func (f *project) dealImportPlaybook(p kkprojectv1.Play, basePlaybook string) er
|
||||||
if importPlaybook == "" {
|
if importPlaybook == "" {
|
||||||
return errors.Errorf("failed to find import_playbook %q base on %q. it's should be:\n %s", p.ImportPlaybook, basePlaybook, PathFormatImportPlaybook)
|
return errors.Errorf("failed to find import_playbook %q base on %q. it's should be:\n %s", p.ImportPlaybook, basePlaybook, PathFormatImportPlaybook)
|
||||||
}
|
}
|
||||||
if err := f.loadPlaybook(importPlaybook); err != nil {
|
if basePlaybook == importPlaybook {
|
||||||
|
// play book import self
|
||||||
|
return errors.Errorf("failed to import %s because it is already imported", p.ImportPlaybook)
|
||||||
|
}
|
||||||
|
if err := f.loadPlaybook(basePlaybook, importPlaybook); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
// KahnGraph represents a directed graph and provides efficient cycle detection using Kahn's algorithm.
|
||||||
|
// Kahn's algorithm repeatedly removes nodes with in-degree 0 (i.e., nodes with no dependencies).
|
||||||
|
// If a cycle exists, nodes in the cycle will never have in-degree 0, so the algorithm cannot process all nodes, thus detecting the presence of a cycle.
|
||||||
|
type KahnGraph struct {
|
||||||
|
// edges is an adjacency list: each node stores all its outgoing edges (target nodes).
|
||||||
|
// key: source node, value: slice of target nodes
|
||||||
|
edges map[string][]string
|
||||||
|
|
||||||
|
// indegree records the in-degree (number of incoming edges) for each node.
|
||||||
|
// key: node name, value: in-degree count
|
||||||
|
indegree map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKahnGraph initializes and returns an empty KahnGraph.
|
||||||
|
func NewKahnGraph() *KahnGraph {
|
||||||
|
return &KahnGraph{
|
||||||
|
edges: make(map[string][]string),
|
||||||
|
indegree: make(map[string]int),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddEdgeAndCheckCycle adds a directed edge from node a to node b and immediately checks if a cycle is formed.
|
||||||
|
// Parameters:
|
||||||
|
// - a: source node
|
||||||
|
// - b: target node
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - true if adding the edge creates a cycle; false otherwise
|
||||||
|
func (g *KahnGraph) AddEdgeAndCheckCycle(a, b string) bool {
|
||||||
|
// Add an edge from a to b
|
||||||
|
g.edges[a] = append(g.edges[a], b)
|
||||||
|
|
||||||
|
// Increment the in-degree of b (one more edge points to b)
|
||||||
|
g.indegree[b]++
|
||||||
|
|
||||||
|
// Ensure a is present in the in-degree map (initialize to 0 if not present)
|
||||||
|
if _, exists := g.indegree[a]; !exists {
|
||||||
|
g.indegree[a] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// After adding the new edge, check if a cycle exists
|
||||||
|
return g.hasCycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasCycle determines whether the current graph contains a cycle.
|
||||||
|
// Returns:
|
||||||
|
// - true if a cycle exists; false otherwise
|
||||||
|
func (g *KahnGraph) hasCycle() bool {
|
||||||
|
// Make a copy of the in-degree map to avoid modifying the original data
|
||||||
|
indegreeCopy := make(map[string]int, len(g.indegree))
|
||||||
|
for node, degree := range g.indegree {
|
||||||
|
indegreeCopy[node] = degree
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a queue to collect all nodes with in-degree 0
|
||||||
|
queue := make([]string, 0)
|
||||||
|
for node, degree := range indegreeCopy {
|
||||||
|
if degree == 0 {
|
||||||
|
queue = append(queue, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processed := 0 // Count of nodes processed by topological sort
|
||||||
|
|
||||||
|
// Process all nodes with in-degree 0 and update their neighbors' in-degree
|
||||||
|
for len(queue) > 0 {
|
||||||
|
// Dequeue the first node
|
||||||
|
node := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
processed++
|
||||||
|
|
||||||
|
// For each neighbor, decrement its in-degree
|
||||||
|
for _, neighbor := range g.edges[node] {
|
||||||
|
indegreeCopy[neighbor]--
|
||||||
|
// If neighbor's in-degree becomes 0, add it to the queue
|
||||||
|
if indegreeCopy[neighbor] == 0 {
|
||||||
|
queue = append(queue, neighbor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all nodes are processed, there is no cycle; otherwise, a cycle exists
|
||||||
|
return processed != len(indegreeCopy)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGraphHasCycle(t *testing.T) {
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
graph KahnGraph
|
||||||
|
except bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
// a -> b
|
||||||
|
name: "Single head, single depth, no cycle",
|
||||||
|
graph: KahnGraph{
|
||||||
|
edges: map[string][]string{
|
||||||
|
"a": {"b"},
|
||||||
|
},
|
||||||
|
indegree: map[string]int{
|
||||||
|
"a": 0,
|
||||||
|
"b": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
except: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// a -> a
|
||||||
|
name: "Single head, single depth, has cycle",
|
||||||
|
graph: KahnGraph{
|
||||||
|
edges: map[string][]string{
|
||||||
|
"a": {"a"},
|
||||||
|
},
|
||||||
|
indegree: map[string]int{
|
||||||
|
"a": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
except: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// a -> b
|
||||||
|
// a -> c
|
||||||
|
name: "Multiple heads, single depth, no cycle",
|
||||||
|
graph: KahnGraph{
|
||||||
|
edges: map[string][]string{
|
||||||
|
"a": {"b", "c"},
|
||||||
|
},
|
||||||
|
indegree: map[string]int{
|
||||||
|
"a": 0,
|
||||||
|
"b": 1,
|
||||||
|
"c": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
except: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// a -> b
|
||||||
|
// a -> a
|
||||||
|
name: "Multiple heads, single depth, has cycle",
|
||||||
|
graph: KahnGraph{
|
||||||
|
edges: map[string][]string{
|
||||||
|
"a": {"b", "a"},
|
||||||
|
},
|
||||||
|
indegree: map[string]int{
|
||||||
|
"a": 1,
|
||||||
|
"b": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
except: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// a -> b -> c
|
||||||
|
name: "Single head, multiple depth, no cycle",
|
||||||
|
graph: KahnGraph{
|
||||||
|
edges: map[string][]string{
|
||||||
|
"a": {"b"},
|
||||||
|
"b": {"c"},
|
||||||
|
},
|
||||||
|
indegree: map[string]int{
|
||||||
|
"a": 0,
|
||||||
|
"b": 1,
|
||||||
|
"c": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
except: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// a -> b -> a
|
||||||
|
name: "Single head, multiple depth, has cycle",
|
||||||
|
graph: KahnGraph{
|
||||||
|
edges: map[string][]string{
|
||||||
|
"a": {"b"},
|
||||||
|
"b": {"a"},
|
||||||
|
},
|
||||||
|
indegree: map[string]int{
|
||||||
|
"a": 1,
|
||||||
|
"b": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
except: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// a -> b
|
||||||
|
// a -> c -> d
|
||||||
|
// a -> d -> b
|
||||||
|
name: "Multiple heads, multiple depth, no cycle",
|
||||||
|
graph: KahnGraph{
|
||||||
|
edges: map[string][]string{
|
||||||
|
"a": {"b", "c", "d"},
|
||||||
|
"c": {"d"},
|
||||||
|
"d": {"b"},
|
||||||
|
},
|
||||||
|
indegree: map[string]int{
|
||||||
|
"a": 0,
|
||||||
|
"b": 2,
|
||||||
|
"c": 1,
|
||||||
|
"d": 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
except: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// a -> b
|
||||||
|
// a -> c -> d
|
||||||
|
// a -> d -> c
|
||||||
|
name: "Multiple heads, multiple depth, has cycle",
|
||||||
|
graph: KahnGraph{
|
||||||
|
edges: map[string][]string{
|
||||||
|
"a": {"b", "c", "d"},
|
||||||
|
"c": {"d"},
|
||||||
|
"d": {"c"},
|
||||||
|
},
|
||||||
|
indegree: map[string]int{
|
||||||
|
"a": 0,
|
||||||
|
"b": 1,
|
||||||
|
"c": 2,
|
||||||
|
"d": 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
except: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tc.except, tc.graph.hasCycle())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue