Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

detect cycle in cli custom commands #765

Closed
wants to merge 14 commits into from
67 changes: 67 additions & 0 deletions cmd/cmd_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,69 @@ func WithStackValidation(check bool) AtmosValidateOption {
}
}

func detectCycle(commands []schema.Command) bool {
// Build a command graph
graph := make(map[string][]string)
for _, cmd := range commands {
for _, step := range cmd.Steps {
// Add an edge from command to each command it depends on
graph[cmd.Name] = append(graph[cmd.Name], parseCommandName(step))
}
}

// To track visited nodes and detect cycles
visited := make(map[string]bool)
recStack := make(map[string]bool)

// Run DFS for each command to detect cycles
for cmd := range graph {
if detectCycleUtil(cmd, graph, visited, recStack) {
return true // Cycle detected
}
}
return false // No cycle detected
}
pkbhowmick marked this conversation as resolved.
Show resolved Hide resolved

func detectCycleUtil(command string, graph map[string][]string, visited, recStack map[string]bool) bool {
// If the current command is in the recursion stack, there's a cycle
if recStack[command] {
return true
}

// If already visited, no need to explore again
if visited[command] {
return false
}

// Mark as visited and add to recursion stack
visited[command] = true
recStack[command] = true

// Recurse for all dependencies
for _, dep := range graph[command] {
if detectCycleUtil(dep, graph, visited, recStack) {
return true
}
}

// Remove from recursion stack before backtracking
recStack[command] = false
return false
}

// Helper function to parse command name from the step
func parseCommandName(step string) string {
// Split the step into parts
parts := strings.Split(step, " ")

// Check if the command starts with "atmos" and has additional parts
if len(parts) > 1 && parts[0] == "atmos" {
// Return everything after "atmos" as a single string
return strings.Join(parts[1:], " ")
}
return ""
}
pkbhowmick marked this conversation as resolved.
Show resolved Hide resolved

// processCustomCommands processes and executes custom commands
func processCustomCommands(
cliConfig schema.CliConfiguration,
Expand All @@ -47,6 +110,10 @@ func processCustomCommands(
existingTopLevelCommands = getTopLevelCommands()
}

if detectCycle(commands) {
return errors.New("cycle detected in custom cli commands")
}
pkbhowmick marked this conversation as resolved.
Show resolved Hide resolved

for _, commandCfg := range commands {
// Clone the 'commandCfg' struct into a local variable because of the automatic closure in the `Run` function of the Cobra command.
// Cloning will make a closure over the local variable 'commandConfig' which is different in each iteration.
Expand Down