-
Notifications
You must be signed in to change notification settings - Fork 137
[SREP-4791]: Add sre-agent integration to osdctl AI commands #890
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
base: master
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 |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package ai | ||
|
|
||
| import ( | ||
| sreagent "github.com/openshift/osdctl/cmd/ai/sre_agent" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| // NewCmdAI implements the base AI command | ||
| func NewCmdAI() *cobra.Command { | ||
| aiCmd := &cobra.Command{ | ||
| Use: "ai", | ||
| Short: "AI-powered tools for SRE automation", | ||
| Args: cobra.NoArgs, | ||
| } | ||
|
|
||
| aiCmd.AddCommand(sreagent.NewCmdSreAgent()) | ||
|
|
||
| return aiCmd | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package sreagent | ||
|
|
||
| import ( | ||
| "bufio" | ||
| "fmt" | ||
| "os" | ||
| "os/exec" | ||
| "strings" | ||
| ) | ||
|
|
||
| // copyRepository copies a directory recursively | ||
| func copyRepository(sourcePath, destPath string) error { | ||
| fmt.Fprintf(os.Stderr, "Copying repository to %s...\n", destPath) | ||
| cmd := exec.Command("cp", "-r", sourcePath, destPath) | ||
| cmd.Stdout = os.Stderr | ||
| cmd.Stderr = os.Stderr | ||
|
|
||
| if err := cmd.Run(); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // promptUserInput reads a line of user input from stdin | ||
| func promptUserInput() (string, error) { | ||
| reader := bufio.NewReader(os.Stdin) | ||
| input, err := reader.ReadString('\n') | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to read input: %w", err) | ||
| } | ||
| return strings.ToLower(strings.TrimSpace(input)), nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| package sreagent | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| cmdutil "k8s.io/kubectl/pkg/cmd/util" | ||
| ) | ||
|
|
||
| var ( | ||
| pdURL string | ||
| autoExecute bool | ||
| outputDir string | ||
| ) | ||
|
|
||
| const ( | ||
| sreAgentDescription = ` | ||
| SRE Agent is an AI-powered tool that helps SREs triage alerts and diagnose issues. | ||
| It automatically fetches incident details from PagerDuty, finds relevant SOPs, | ||
| and executes diagnostic commands on clusters. | ||
| ` | ||
|
|
||
| sreAgentExample = ` | ||
| # Interactive mode (asks for confirmation at each step) | ||
| osdctl ai sre-agent --pd-url "${PD_URL}" | ||
|
|
||
| # Fully automated mode (no confirmations) | ||
| osdctl ai sre-agent --pd-url "${PD_URL}" --auto-execute | ||
|
|
||
| # Specify output directory for sre-agent files | ||
| osdctl ai sre-agent --pd-url "${PD_URL}" --output /tmp/sre-agent-output | ||
| ` | ||
| ) | ||
|
|
||
| func NewCmdSreAgent() *cobra.Command { | ||
| sreAgentCmd := &cobra.Command{ | ||
| Use: "sre-agent", | ||
| Short: "Run SRE Agent for automated incident investigation", | ||
| Long: sreAgentDescription, | ||
| Example: sreAgentExample, | ||
| Args: cobra.ArbitraryArgs, | ||
| SilenceUsage: true, | ||
| SilenceErrors: true, | ||
| Run: func(cmd *cobra.Command, args []string) { | ||
| homeDir, err := os.UserHomeDir() | ||
| if err != nil { | ||
| cmdutil.CheckErr(fmt.Errorf("failed to get home directory: %w", err)) | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| // Step 1: Validate sre-agent installation | ||
| if !validateSreAgent(homeDir) { | ||
| return | ||
| } | ||
|
|
||
| // Step 2: Check/Setup config (includes ops-sop setup) | ||
| if !checkSreAgentConfig(homeDir) { | ||
| return | ||
| } | ||
|
|
||
| // Step 3: Execute sre-agent | ||
| sreAgentPath := filepath.Join(homeDir, ".local/share/sre-agent/venv/bin/sre-agent") | ||
| sreAgentArgs := buildSreAgentArgs(args) | ||
|
|
||
| err = executeSreAgent(sreAgentPath, sreAgentArgs, outputDir) | ||
| if err != nil { | ||
| cmdutil.CheckErr(err) | ||
| } | ||
| }, | ||
| } | ||
|
|
||
| sreAgentCmd.Flags().StringVar(&pdURL, "pd-url", "", "PagerDuty incident URL (required)") | ||
| sreAgentCmd.Flags().BoolVar(&autoExecute, "auto-execute", false, "Fully automated mode without confirmations") | ||
| sreAgentCmd.Flags().StringVar(&outputDir, "output", "", "Output directory for sre-agent files (default: current directory)") | ||
|
|
||
| // Mark pd-url as required | ||
| if err := sreAgentCmd.MarkFlagRequired("pd-url"); err != nil { | ||
| fmt.Fprintf(os.Stderr, "Failed to mark pd-url as required: %v\n", err) | ||
| } | ||
|
|
||
| return sreAgentCmd | ||
| } | ||
|
|
||
| // buildSreAgentArgs constructs the argument list for sre-agent command | ||
| func buildSreAgentArgs(additionalArgs []string) []string { | ||
| args := []string{} | ||
|
|
||
| if pdURL != "" { | ||
| args = append(args, "--pd-url", pdURL) | ||
| } | ||
|
|
||
| if autoExecute { | ||
| args = append(args, "--auto-execute") | ||
| } | ||
|
|
||
| // Add any additional arguments passed | ||
| args = append(args, additionalArgs...) | ||
|
|
||
| return args | ||
| } | ||
|
|
||
| // executeSreAgent runs the sre-agent command with provided arguments | ||
| func executeSreAgent(sreAgentPath string, args []string, outputDir string) error { | ||
| cmd := exec.Command(sreAgentPath, args...) | ||
| cmd.Stdout = os.Stdout | ||
| cmd.Stderr = os.Stderr | ||
| cmd.Stdin = os.Stdin | ||
|
|
||
| // Set working directory if output directory is specified | ||
| if outputDir != "" { | ||
| // Create directory if it doesn't exist | ||
| if err := os.MkdirAll(outputDir, 0755); err != nil { | ||
| return fmt.Errorf("failed to create output directory: %w", err) | ||
| } | ||
| cmd.Dir = outputDir | ||
| } | ||
|
|
||
| if err := cmd.Run(); err != nil { | ||
| return fmt.Errorf("sre-agent execution failed: %w", err) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package sreagent | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
|
|
||
| "github.com/openshift/osdctl/internal/utils" | ||
| cmdutil "k8s.io/kubectl/pkg/cmd/util" | ||
| ) | ||
|
|
||
| // validateSreAgent checks if sre-agent is installed | ||
| func validateSreAgent(homeDir string) bool { | ||
| baseDir := filepath.Join(homeDir, ".local/share/sre-agent") | ||
| venvBinary := filepath.Join(baseDir, "venv/bin/sre-agent") | ||
|
|
||
| // Check if sre-agent binary exists | ||
| if utils.FileExists(venvBinary) { | ||
| return true // Already installed | ||
| } | ||
|
|
||
| fmt.Fprintf(os.Stderr, "sre-agent is not found in ~/.local/share/sre-agent/venv/\n\n") | ||
|
|
||
| // Ask for path to sre-agent venv | ||
| fmt.Fprint(os.Stderr, "Enter the absolute path to sre-agent venv directory: ") | ||
| userVenvPath, err := promptUserInput() | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "Failed to read input: %v\n", err) | ||
| return false | ||
| } | ||
|
Comment on lines
+24
to
+30
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. Validate that the user-provided path is absolute. The prompt on line 25 explicitly requests an "absolute path", but there's no validation to ensure the provided 🛡️ Proposed fix to validate absolute path // Ask for path to sre-agent venv
fmt.Fprint(os.Stderr, "Enter the absolute path to sre-agent venv directory: ")
userVenvPath, err := promptUserInput()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to read input: %v\n", err)
return false
}
+ if !filepath.IsAbs(userVenvPath) {
+ fmt.Fprintln(os.Stderr, "Error: path must be absolute")
+ return false
+ }
// Validate venv binary exists in provided path🤖 Prompt for AI Agents |
||
|
|
||
| // Validate venv binary exists in provided path | ||
| userVenvBinary := filepath.Join(userVenvPath, "bin/sre-agent") | ||
| if !utils.FileExists(userVenvBinary) { | ||
| fmt.Fprintln(os.Stderr, "\nsre-agent isn't installed") | ||
| return false | ||
| } | ||
|
|
||
| // Create base directory | ||
| if err := os.MkdirAll(baseDir, 0755); err != nil { | ||
| cmdutil.CheckErr(fmt.Errorf("failed to create base directory: %w", err)) | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| // Copy venv to ~/.local/share/sre-agent/venv | ||
| venvPath := filepath.Join(baseDir, "venv") | ||
| if err := copyRepository(userVenvPath, venvPath); err != nil { | ||
| fmt.Fprintf(os.Stderr, "\nCopy failed: %v\n", err) | ||
| return false | ||
| } | ||
|
|
||
| fmt.Fprintln(os.Stderr, "\n✓ sre-agent venv copied successfully") | ||
| return true | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| package sreagent | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
|
|
||
| "github.com/openshift/osdctl/internal/utils" | ||
| "gopkg.in/yaml.v3" | ||
| ) | ||
|
|
||
| // checkSreAgentConfig validates config.yaml and updates ops-sop path if needed | ||
| func checkSreAgentConfig(homeDir string) bool { | ||
| baseDir := filepath.Join(homeDir, ".local/share/sre-agent") | ||
| configPath := filepath.Join(homeDir, ".config/sre-agent/config.yaml") | ||
|
|
||
| // Check if config exists | ||
| if !utils.FileExists(configPath) { | ||
| fmt.Fprintln(os.Stderr, "\nsre-agent not configured") | ||
| fmt.Fprintln(os.Stderr, "Config file not found at:", configPath) | ||
| return false | ||
| } | ||
|
|
||
| // Read existing config | ||
| data, err := os.ReadFile(configPath) | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "Failed to read config: %v\n", err) | ||
| return false | ||
| } | ||
|
|
||
| // Parse YAML | ||
| var config map[string]interface{} | ||
| if err := yaml.Unmarshal(data, &config); err != nil { | ||
| fmt.Fprintf(os.Stderr, "Failed to parse config: %v\n", err) | ||
| return false | ||
| } | ||
|
|
||
| // Get current sop directory from config | ||
| sop, ok := config["sop"].(map[string]interface{}) | ||
| if !ok { | ||
| fmt.Fprintln(os.Stderr, "Invalid config: sop section not found") | ||
| return false | ||
| } | ||
|
|
||
| currentSopDir, ok := sop["directory"].(string) | ||
| if !ok { | ||
| fmt.Fprintln(os.Stderr, "Invalid config: sop directory is not a string") | ||
| return false | ||
| } | ||
|
|
||
| // Ask user for ops-sop repository path | ||
| fmt.Fprintln(os.Stderr, "\nChecking ops-sop repository...") | ||
| fmt.Fprint(os.Stderr, "Enter the absolute path to ops-sop repository: ") | ||
| userOpsSopPath, err := promptUserInput() | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "Failed to read input: %v\n", err) | ||
| return false | ||
| } | ||
|
|
||
| // Validate path exists | ||
| if !utils.FolderExists(userOpsSopPath) { | ||
| fmt.Fprintln(os.Stderr, "\nThe provided ops-sop path does not exist.") | ||
| return false | ||
| } | ||
|
|
||
| opsSopPath := filepath.Join(baseDir, "ops-sop") | ||
|
|
||
| // Copy ops-sop if not present | ||
| if !utils.FolderExists(opsSopPath) { | ||
| if err := copyRepository(userOpsSopPath, opsSopPath); err != nil { | ||
| fmt.Fprintf(os.Stderr, "\nCopy failed: %v\n", err) | ||
| return false | ||
| } | ||
| fmt.Fprintln(os.Stderr, "✓ ops-sop copied successfully") | ||
| } else { | ||
| fmt.Fprintln(os.Stderr, "✓ ops-sop repository found") | ||
| } | ||
|
|
||
| // Check if sop directory in config is different from expected | ||
| if currentSopDir != opsSopPath { | ||
| // Update config with new path | ||
| sop["directory"] = opsSopPath | ||
|
|
||
| // Write updated config | ||
| updatedData, err := yaml.Marshal(config) | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "Failed to marshal config: %v\n", err) | ||
| return false | ||
| } | ||
|
|
||
| if err := os.WriteFile(configPath, updatedData, 0600); err != nil { | ||
| fmt.Fprintf(os.Stderr, "Failed to write config: %v\n", err) | ||
| return false | ||
| } | ||
|
|
||
| fmt.Fprintf(os.Stderr, "✓ ops-sop path updated in config: %s\n\n", opsSopPath) | ||
| } else { | ||
| fmt.Fprintf(os.Stderr, "✓ ops-sop path is correct: %s\n\n", opsSopPath) | ||
| } | ||
|
|
||
| return true | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.