-
-
Notifications
You must be signed in to change notification settings - Fork 96
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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 "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+86
to
+97
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance command parsing robustness The current parsing logic might miss cycles in complex command formats and could mask issues by returning empty strings. Consider this enhanced implementation: 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:], " ")
+ // Extract the actual command name, handling flags and arguments
+ cmdParts := []string{}
+ for _, part := range parts[1:] {
+ if strings.HasPrefix(part, "-") {
+ break // Stop at first flag
+ }
+ cmdParts = append(cmdParts, part)
+ }
+ return strings.Join(cmdParts, " ")
}
return ""
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// processCustomCommands processes and executes custom commands | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
func processCustomCommands( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
cliConfig schema.CliConfiguration, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -47,6 +110,10 @@ func processCustomCommands( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
existingTopLevelCommands = getTopLevelCommands() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if detectCycle(commands) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return errors.New("cycle detected in custom cli commands") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+113
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance error reporting and implement environment variable tracking The current implementation could provide more helpful error messages and implement the environment variable tracking suggested in the PR comments. Consider this enhancement: +const AtmosCommandDepthEnv = "ATMOS_COMMAND_DEPTH"
+
+func getCommandDepth() int {
+ depth := os.Getenv(AtmosCommandDepthEnv)
+ if depth == "" {
+ return 0
+ }
+ val, _ := strconv.Atoi(depth)
+ return val
+}
+
if detectCycle(commands) {
- return errors.New("cycle detected in custom cli commands")
+ return fmt.Errorf("cycle detected in custom CLI commands - this could lead to infinite recursion")
+}
+
+// Implement depth tracking as suggested in PR comments
+depth := getCommandDepth()
+if depth > 10 { // Configure max depth as needed
+ return fmt.Errorf("maximum command depth exceeded (%d) - possible recursive loop", depth)
}
+os.Setenv(AtmosCommandDepthEnv, strconv.Itoa(depth+1))
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @pkbhowmick something like this 👆 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
got it. Thank you |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider adding validation for command existence and depth limiting
The cycle detection implementation is solid, but could be enhanced in a few ways:
Here's a suggested enhancement:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like a good recommendation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@osterman, here's an updated
detectCycle
function incorporating command existence validation and depth limiting:You'll need to define an appropriate
maxDepth
when callingdetectCycle
to enforce the recursion limit.