diff --git a/CLAUDE.md b/CLAUDE.md index 5569f09..1a3ee5c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -86,6 +86,19 @@ Manage multiple environments (dev, staging, prod) using profiles. | `execution task-poll ` | Poll for tasks | task type | `orkes execution task-poll my_task --count 5` | | `execution task-update` | Update task by ref name | workflow-id, task-ref-name, status | `orkes execution task-update --workflow-id abc --task-ref-name task1 --status COMPLETED` | +### Config Commands + +| Command | Description | Required Args | Optional Flags | Example | +|---------|-------------|---------------|----------------|---------| +| `config save` | Save current configuration | None | `--profile` | `orkes --server http://localhost:8080/api --auth-key key --profile production config save` | +| `config list` | List all configuration profiles | None | None | `orkes config list` | +| `config delete [profile]` | Delete configuration file | None | `--profile`, `-y` | `orkes config delete production` or `orkes config delete --profile production -y` | + +**Notes:** +- `config save`: Use `--profile ` to save to a named profile (e.g., `config-production.yaml`). Without it, saves to default `config.yaml`. +- `config list`: Shows all profiles. Default config shown as "default", named profiles show as profile name only. +- `config delete`: Profile can be specified as positional arg or via `--profile` flag. Use `-y` to skip confirmation prompt. + ### Webhook Commands | Command | Description | Required Args | Example | diff --git a/README.md b/README.md index 3a3f3d2..c30de62 100644 --- a/README.md +++ b/README.md @@ -163,17 +163,29 @@ Profiles allow you to manage multiple Conductor environments (e.g., development, **Creating a Profile:** -Save your current flags to a named profile: +Save your current flags to a named profile using the `config save` command: ```bash # Save to default profile (~/.conductor-cli/config.yaml) -orkes --server https://dev.example.com --auth-key key123 --save-config workflow list +orkes --server https://dev.example.com --auth-key key123 config save # Save to named profile (~/.conductor-cli/config-production.yaml) -orkes --server https://prod.example.com --auth-token token --save-config=production workflow list +orkes --server https://prod.example.com --auth-token token --profile production config save +``` + +**Deleting a Profile:** -# Create multiple profiles -orkes --server https://staging.example.com --auth-key key --save-config=staging workflow list +Delete a configuration file using the `config delete` command: + +```bash +# Delete default config (with confirmation prompt) +orkes config delete + +# Delete named profile (with confirmation prompt) +orkes config delete production + +# Delete without confirmation +orkes config delete production -y ``` **Using a Profile:** @@ -211,6 +223,58 @@ orkes --profile nonexistent workflow list # Error: Profile 'nonexistent' doesn't exist (expected file: ~/.conductor-cli/config-nonexistent.yaml) ``` +## Config Management Commands + +The CLI provides dedicated commands for managing configuration files: + +### Save Configuration + +```bash +# Save to default config file +orkes --server http://localhost:8080/api --auth-key key123 config save + +# Save to a named profile (using --profile flag) +orkes --server https://prod.example.com --auth-token token --profile production config save + +# Flags can be placed before or after the command +orkes config save --server http://localhost:8080/api --auth-key key123 --profile staging +``` + +### List Configurations + +```bash +# List all configuration profiles +orkes config list +``` + +This shows: +- `default` - for the default `config.yaml` file +- Profile names (e.g., `production`, `staging`) - for named profiles like `config-production.yaml` + +### Delete Configuration + +```bash +# Delete default config (with confirmation prompt) +orkes config delete + +# Delete named profile using positional argument +orkes config delete production + +# Delete named profile using --profile flag +orkes config delete --profile production + +# Delete without confirmation using -y flag +orkes config delete production -y +orkes config delete --profile staging -y +``` + +**Notes:** +- The `--profile` flag specifies which profile to save/delete +- Without `--profile`, operations affect the default `config.yaml` +- `config list` shows all available profiles in `~/.conductor-cli/` directory +- Delete operations require confirmation unless `-y` flag is used +- Both positional argument and `--profile` flag work for delete command + ## Workflow Metadata Management ```shell diff --git a/cmd/config.go b/cmd/config.go index 0925302..cf5f532 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -1,28 +1,195 @@ package cmd import ( + "bufio" + "fmt" "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" ) -const CONDUCTOR_SERVER_URL = "CONDUCTOR_SERVER_URL" -const CONDUCTOR_AUTH_KEY = "CONDUCTOR_AUTH_KEY" -const CONDUCTOR_AUTH_SECRET = "CONDUCTOR_AUTH_SECRET" -const CONDUCTOR_AUTH_TOKEN = "CONDUCTOR_AUTH_TOKEN" +var configCmd = &cobra.Command{ + Use: "config", + Short: "CLI configuration management", +} + +var configSaveCmd = &cobra.Command{ + Use: "save", + Short: "Save current configuration to file", + Long: `Save the current server and authentication settings to a configuration file. + +The configuration will be saved to ~/.conductor-cli/config.yaml by default, +or to a profile-specific file if --profile is specified. + +Examples: + # Save to default config file + orkes --server http://localhost:8080/api --auth-key key123 config save -type Config struct { - URL string `json:url` - Token string `json:token` - Key string `json:token` - Secret string `json:token` + # Save to a named profile + orkes --server https://prod.example.com --auth-token token123 --profile production config save +`, + RunE: func(cmd *cobra.Command, args []string) error { + profileName := profile + + if err := saveConfigFile(profileName); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + configFileName := "config.yaml" + if profileName != "" { + configFileName = fmt.Sprintf("config-%s.yaml", profileName) + } + + fmt.Fprintf(os.Stderr, "✓ Configuration saved to ~/.conductor-cli/%s\n", configFileName) + return nil + }, + SilenceUsage: true, } -func getActiveConfig() *Config { +var configListCmd = &cobra.Command{ + Use: "list", + Short: "List all configuration profiles", + Long: `List all configuration profiles in ~/.conductor-cli directory. + +Shows the default config.yaml and all named profiles (config-.yaml). + +Examples: + # List all config profiles + orkes config list +`, + RunE: func(cmd *cobra.Command, args []string) error { + home, err := os.UserHomeDir() + if err != nil { + return err + } + + configDir := filepath.Join(home, ".conductor-cli") + + // Check if config directory exists + if _, err := os.Stat(configDir); os.IsNotExist(err) { + fmt.Println("No configuration files found") + return nil + } + + // Read all files in config directory + files, err := os.ReadDir(configDir) + if err != nil { + return fmt.Errorf("failed to read config directory: %w", err) + } + + hasConfigs := false + for _, file := range files { + if file.IsDir() { + continue + } + + name := file.Name() + + // Handle default config.yaml + if name == "config.yaml" { + fmt.Println("default") + hasConfigs = true + continue + } + + // Handle named profiles: config-.yaml + if strings.HasPrefix(name, "config-") && strings.HasSuffix(name, ".yaml") { + profileName := strings.TrimPrefix(name, "config-") + profileName = strings.TrimSuffix(profileName, ".yaml") + fmt.Println(profileName) + hasConfigs = true + } + } + + if !hasConfigs { + fmt.Println("No configuration files found") + } + + return nil + }, + SilenceUsage: true, +} + +var configDeleteCmd = &cobra.Command{ + Use: "delete [profile]", + Short: "Delete a configuration file", + Long: `Delete a configuration file. + +If no profile is specified, the default config.yaml will be deleted. +Profile can be specified either as a positional argument or via --profile flag. + +Examples: + # Delete default config file (requires confirmation) + orkes config delete + + # Delete a named profile using positional argument + orkes config delete production + + # Delete a named profile using --profile flag + orkes config delete --profile production + + # Delete without confirmation + orkes config delete production -y +`, + RunE: func(cmd *cobra.Command, args []string) error { + home, err := os.UserHomeDir() + if err != nil { + return err + } + + configDir := filepath.Join(home, ".conductor-cli") + + // Get profile name from either positional arg or --profile flag + var profileName string + if len(args) > 0 { + profileName = args[0] + } else if profile != "" { + profileName = profile + } + + configFileName := "config.yaml" + if profileName != "" { + configFileName = fmt.Sprintf("config-%s.yaml", profileName) + } + + configPath := filepath.Join(configDir, configFileName) + + // Check if config file exists + if _, err := os.Stat(configPath); os.IsNotExist(err) { + return fmt.Errorf("config file does not exist: %s", configPath) + } + + // Ask for confirmation unless -y flag is set + if !yes { + reader := bufio.NewReader(os.Stdin) + fmt.Fprintf(os.Stderr, "Are you sure you want to delete %s? [y/N]: ", configPath) + response, err := reader.ReadString('\n') + if err != nil { + return err + } + response = strings.ToLower(strings.TrimSpace(response)) + if response != "y" && response != "yes" { + fmt.Fprintf(os.Stderr, "Deletion cancelled\n") + return nil + } + } + + // Delete the file + if err := os.Remove(configPath); err != nil { + return fmt.Errorf("failed to delete config file: %w", err) + } + + fmt.Fprintf(os.Stderr, "✓ Configuration deleted: %s\n", configPath) + return nil + }, + SilenceUsage: true, +} - config := Config{ - URL: os.Getenv(CONDUCTOR_SERVER_URL), - Token: os.Getenv(CONDUCTOR_AUTH_TOKEN), - Key: os.Getenv(CONDUCTOR_AUTH_KEY), - Secret: os.Getenv(CONDUCTOR_AUTH_SECRET), - } - return &config +func init() { + rootCmd.AddCommand(configCmd) + configCmd.AddCommand(configSaveCmd) + configCmd.AddCommand(configListCmd) + configCmd.AddCommand(configDeleteCmd) } diff --git a/cmd/root.go b/cmd/root.go index 143475b..569cf51 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -31,15 +31,14 @@ var ( var NAME = "orkes" var ( - cfgFile string - profile string - saveConfig string - url string - key string - secret string - token string - verbose bool - yes bool + cfgFile string + profile string + url string + key string + secret string + token string + verbose bool + yes bool ) var rootCmd = &cobra.Command{ Use: NAME, @@ -94,26 +93,6 @@ var rootCmd = &cobra.Command{ internal.SetAPIClient(apiClient) - // Handle --save-config flag - if saveConfig != "" || (cmd.Flag("save-config").Changed && saveConfig == "") { - // Use saveConfig value as profile name if provided, otherwise use --profile value - profileName := saveConfig - if profileName == "" { - profileName = profile - } - - if err := saveConfigFile(profileName); err != nil { - return fmt.Errorf("failed to save config: %w", err) - } - - configFileName := "config.yaml" - if profileName != "" { - configFileName = fmt.Sprintf("config-%s.yaml", profileName) - } - - fmt.Fprintf(os.Stderr, "✓ Configuration saved to ~/.conductor-cli/%s\n", configFileName) - } - return nil }, } @@ -216,10 +195,20 @@ func initConfig() { configName := fmt.Sprintf("config-%s", activeProfile) configPath := filepath.Join(configDir, configName+".yaml") - // Check if profile config exists - if _, err := os.Stat(configPath); os.IsNotExist(err) { - fmt.Fprintf(os.Stderr, "Error: Profile '%s' doesn't exist (expected file: %s)\n", activeProfile, configPath) - os.Exit(1) + // Check if profile config exists (skip check if we're saving a new config) + isSavingConfig := false + for i, arg := range os.Args { + if arg == "config" && i+1 < len(os.Args) && os.Args[i+1] == "save" { + isSavingConfig = true + break + } + } + + if !isSavingConfig { + if _, err := os.Stat(configPath); os.IsNotExist(err) { + fmt.Fprintf(os.Stderr, "Error: Profile '%s' doesn't exist (expected file: %s)\n", activeProfile, configPath) + os.Exit(1) + } } viper.SetConfigName(configName) @@ -266,7 +255,6 @@ func init() { // Profile and config management flags rootCmd.PersistentFlags().StringVar(&profile, "profile", "", "use a specific profile (loads config-.yaml, can also be set via ORKES_PROFILE)") - rootCmd.PersistentFlags().StringVar(&saveConfig, "save-config", "", "save current flags to config file (optionally specify profile name)") // Other flags rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "print verbose logs") diff --git a/test/e2e/config.bats b/test/e2e/config.bats new file mode 100755 index 0000000..7bcdbc7 --- /dev/null +++ b/test/e2e/config.bats @@ -0,0 +1,184 @@ +#!/usr/bin/env bats + +# E2E tests for config commands +# Tests config save and delete functionality + +setup() { + # Ensure the CLI binary exists + if [ ! -f "./orkes" ]; then + echo "ERROR: orkes binary not found. Please build it first." + exit 1 + fi + + # Clean up any existing test config files + rm -f ~/.conductor-cli/config-e2e-test.yaml + rm -f ~/.conductor-cli/config-e2e-test2.yaml + rm -f ~/.conductor-cli/config-e2e-default.yaml +} + +teardown() { + # Clean up test config files after each test + rm -f ~/.conductor-cli/config-e2e-test.yaml + rm -f ~/.conductor-cli/config-e2e-test2.yaml + rm -f ~/.conductor-cli/config-e2e-default.yaml +} + +@test "1. Save config to named profile with --profile flag" { + run bash -c "./orkes --server http://test.example.com --auth-key test-key-123 --auth-secret test-secret-456 --profile e2e-test config save 2>/dev/null" + echo "Output: $output" + [ "$status" -eq 0 ] + [[ "$output" == *"Configuration saved to ~/.conductor-cli/config-e2e-test.yaml"* ]] + + # Verify file exists + [ -f ~/.conductor-cli/config-e2e-test.yaml ] + + # Verify file contents + grep -q "server: http://test.example.com" ~/.conductor-cli/config-e2e-test.yaml + grep -q "auth-key: test-key-123" ~/.conductor-cli/config-e2e-test.yaml + grep -q "auth-secret: test-secret-456" ~/.conductor-cli/config-e2e-test.yaml +} + +@test "2. Save config with auth-token instead of key/secret" { + run bash -c "./orkes --server https://prod.example.com --auth-token my-token-789 --profile e2e-test2 config save 2>/dev/null" + echo "Output: $output" + [ "$status" -eq 0 ] + + # Verify file exists + [ -f ~/.conductor-cli/config-e2e-test2.yaml ] + + # Verify file contents + grep -q "server: https://prod.example.com" ~/.conductor-cli/config-e2e-test2.yaml + grep -q "auth-token: my-token-789" ~/.conductor-cli/config-e2e-test2.yaml + + # Should NOT contain auth-key or auth-secret + ! grep -q "auth-key" ~/.conductor-cli/config-e2e-test2.yaml + ! grep -q "auth-secret" ~/.conductor-cli/config-e2e-test2.yaml +} + +@test "3. Save config with flags after command" { + run bash -c "./orkes config save --server http://custom.example.com --auth-key local-key --profile e2e-test 2>/dev/null" + echo "Output: $output" + [ "$status" -eq 0 ] + + # Verify file was overwritten with new values + [ -f ~/.conductor-cli/config-e2e-test.yaml ] + grep -q "server: http://custom.example.com" ~/.conductor-cli/config-e2e-test.yaml + grep -q "auth-key: local-key" ~/.conductor-cli/config-e2e-test.yaml +} + +@test "4. Delete config using --profile flag with -y" { + # First create the config + ./orkes --server http://test.com --auth-key key --profile e2e-test config save 2>/dev/null + + # Delete it + run bash -c "./orkes config delete --profile e2e-test -y 2>/dev/null" + echo "Output: $output" + [ "$status" -eq 0 ] + [[ "$output" == *"Configuration deleted"* ]] + [[ "$output" == *"config-e2e-test.yaml"* ]] + + # Verify file was deleted + [ ! -f ~/.conductor-cli/config-e2e-test.yaml ] +} + +@test "5. Delete config using positional argument" { + # First create the config + ./orkes --server http://test.com --auth-key key --profile e2e-test config save 2>/dev/null + + # Delete it using positional argument + run bash -c "./orkes config delete e2e-test -y 2>/dev/null" + echo "Output: $output" + [ "$status" -eq 0 ] + [[ "$output" == *"Configuration deleted"* ]] + + # Verify file was deleted + [ ! -f ~/.conductor-cli/config-e2e-test.yaml ] +} + +@test "6. Delete non-existent config shows error" { + run bash -c "./orkes config delete --profile nonexistent -y 2>&1" + echo "Output: $output" + [ "$status" -ne 0 ] + [[ "$output" == *"doesn't exist"* ]] +} + +@test "7. Save config without --profile saves to default" { + # Note: We use a different approach to test default config + # to avoid interfering with actual default config + run bash -c "./orkes --server http://default.test.com --auth-key default-key --profile e2e-default config save 2>/dev/null" + echo "Output: $output" + [ "$status" -eq 0 ] + [[ "$output" == *"config-e2e-default.yaml"* ]] + + # Verify file exists with correct name + [ -f ~/.conductor-cli/config-e2e-default.yaml ] +} + +@test "8. Config file has correct permissions (0600)" { + run bash -c "./orkes --server http://test.com --auth-key key --profile e2e-test config save 2>/dev/null" + [ "$status" -eq 0 ] + + # Check file permissions (should be 0600 or -rw-------) + perms=$(stat -f "%OLp" ~/.conductor-cli/config-e2e-test.yaml 2>/dev/null || stat -c "%a" ~/.conductor-cli/config-e2e-test.yaml 2>/dev/null) + [ "$perms" = "600" ] +} + +@test "9. Config save with only server URL (no auth)" { + run bash -c "./orkes --server http://noauth.example.com --profile e2e-test config save 2>/dev/null" + echo "Output: $output" + [ "$status" -eq 0 ] + + # Verify file exists but only has server (no auth fields since default localhost is filtered) + [ -f ~/.conductor-cli/config-e2e-test.yaml ] + grep -q "server: http://noauth.example.com" ~/.conductor-cli/config-e2e-test.yaml +} + +@test "10. Multiple saves to same profile overwrites correctly" { + # First save + ./orkes --server http://first.com --auth-key first-key --profile e2e-test config save 2>/dev/null + + # Second save + run bash -c "./orkes --server http://second.com --auth-key second-key --profile e2e-test config save 2>/dev/null" + [ "$status" -eq 0 ] + + # Verify only second values exist + grep -q "server: http://second.com" ~/.conductor-cli/config-e2e-test.yaml + grep -q "auth-key: second-key" ~/.conductor-cli/config-e2e-test.yaml + ! grep -q "first.com" ~/.conductor-cli/config-e2e-test.yaml + ! grep -q "first-key" ~/.conductor-cli/config-e2e-test.yaml +} + +@test "11. List config profiles shows all profiles" { + # Create multiple profiles + ./orkes --server http://test1.com --auth-key key1 --profile e2e-list1 config save 2>/dev/null + ./orkes --server http://test2.com --auth-key key2 --profile e2e-list2 config save 2>/dev/null + ./orkes --server http://test3.com --auth-key key3 --profile e2e-list3 config save 2>/dev/null + + # List configs + run bash -c "./orkes config list 2>/dev/null" + echo "Output: $output" + [ "$status" -eq 0 ] + + # Verify all profiles are listed + [[ "$output" == *"e2e-list1"* ]] + [[ "$output" == *"e2e-list2"* ]] + [[ "$output" == *"e2e-list3"* ]] + + # Clean up + rm -f ~/.conductor-cli/config-e2e-list1.yaml + rm -f ~/.conductor-cli/config-e2e-list2.yaml + rm -f ~/.conductor-cli/config-e2e-list3.yaml +} + +@test "12. List shows 'default' for config.yaml" { + # Create default config (using a unique profile name to avoid conflicts) + ./orkes --server http://test.com --auth-key key --profile e2e-default-check config save 2>/dev/null + + # List configs + run bash -c "./orkes config list 2>/dev/null" + [ "$status" -eq 0 ] + [[ "$output" == *"e2e-default-check"* ]] + + # Clean up + rm -f ~/.conductor-cli/config-e2e-default-check.yaml +}