Files
gh-vanman2024-cli-builder-p…/skills/cobra-patterns/examples/kubectl-style-cli.md
2025-11-30 09:04:14 +08:00

8.6 KiB

Kubectl-Style CLI Example

This example demonstrates how to build a kubectl-style CLI with nested resource commands and consistent flag handling.

Structure

myctl/
├── cmd/
│   ├── root.go                 # Root command with global flags
│   ├── get/
│   │   ├── get.go             # Parent "get" command
│   │   ├── pods.go            # Get pods subcommand
│   │   ├── services.go        # Get services subcommand
│   │   └── deployments.go     # Get deployments subcommand
│   ├── create/
│   │   ├── create.go          # Parent "create" command
│   │   ├── deployment.go      # Create deployment subcommand
│   │   └── service.go         # Create service subcommand
│   ├── delete/
│   │   └── delete.go          # Delete command (accepts any resource)
│   ├── apply.go               # Apply from file/stdin
│   └── completion.go          # Shell completion
└── main.go

Usage Pattern

# Get resources
myctl get pods
myctl get pods my-pod
myctl get pods --namespace production
myctl get services --all-namespaces

# Create resources
myctl create deployment my-app --image nginx:latest --replicas 3
myctl create service my-svc --port 80 --target-port 8080

# Delete resources
myctl delete pod my-pod
myctl delete deployment my-app --force

# Apply configuration
myctl apply -f deployment.yaml
myctl apply -f config.yaml --dry-run

Key Features

1. Resource-Based Organization

Commands are organized by resource type:

  • get <resource> - Retrieve resources
  • create <resource> - Create resources
  • delete <resource> - Delete resources

2. Consistent Flag Handling

Global flags available to all commands:

  • --namespace, -n - Target namespace
  • --all-namespaces, -A - Query all namespaces
  • --output, -o - Output format (json|yaml|text)
  • --verbose, -v - Verbose logging

3. Command Groups

Organized help output:

Basic Commands:
  get         Display resources
  describe    Show detailed information

Management Commands:
  create      Create resources
  delete      Delete resources
  apply       Apply configuration

Implementation Example

Root Command (cmd/root.go)

package cmd

import (
    "github.com/spf13/cobra"
    "myctl/cmd/get"
    "myctl/cmd/create"
    "myctl/cmd/delete"
)

var (
    namespace      string
    allNamespaces  bool
    output         string
)

var rootCmd = &cobra.Command{
    Use:   "myctl",
    Short: "Kubernetes-style resource management CLI",
}

func Execute() error {
    return rootCmd.Execute()
}

func init() {
    // Global flags
    rootCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "default", "target namespace")
    rootCmd.PersistentFlags().BoolVarP(&allNamespaces, "all-namespaces", "A", false, "query all namespaces")
    rootCmd.PersistentFlags().StringVarP(&output, "output", "o", "text", "output format (text|json|yaml)")

    // Command groups
    rootCmd.AddGroup(&cobra.Group{ID: "basic", Title: "Basic Commands:"})
    rootCmd.AddGroup(&cobra.Group{ID: "management", Title: "Management Commands:"})

    // Register commands
    rootCmd.AddCommand(get.GetCmd)
    rootCmd.AddCommand(create.CreateCmd)
    rootCmd.AddCommand(delete.DeleteCmd)
}

// Helper to get global flags
func GetNamespace() string {
    return namespace
}

func GetAllNamespaces() bool {
    return allNamespaces
}

func GetOutput() string {
    return output
}

Get Command Parent (cmd/get/get.go)

package get

import (
    "github.com/spf13/cobra"
)

var GetCmd = &cobra.Command{
    Use:     "get",
    Short:   "Display resources",
    Long:    `Display one or many resources`,
    GroupID: "basic",
}

func init() {
    // Add resource subcommands
    GetCmd.AddCommand(podsCmd)
    GetCmd.AddCommand(servicesCmd)
    GetCmd.AddCommand(deploymentsCmd)
}

Get Pods Subcommand (cmd/get/pods.go)

package get

import (
    "fmt"

    "github.com/spf13/cobra"
    "myctl/cmd"
    "myctl/internal/client"
)

var (
    selector string
    watch    bool
)

var podsCmd = &cobra.Command{
    Use:   "pods [NAME]",
    Short: "Display pods",
    Long:  `Display one or many pods`,
    Args:  cobra.MaximumNArgs(1),
    ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
        // Dynamic completion: fetch pod names
        return client.ListPodNames(cmd.GetNamespace()), cobra.ShellCompDirectiveNoFileComp
    },
    RunE: func(cmd *cobra.Command, args []string) error {
        namespace := cmd.GetNamespace()
        allNamespaces := cmd.GetAllNamespaces()
        output := cmd.GetOutput()

        if len(args) == 0 {
            // List pods
            return listPods(namespace, allNamespaces, selector, output)
        }

        // Get specific pod
        podName := args[0]
        return getPod(namespace, podName, output)
    },
}

func init() {
    // Command-specific flags
    podsCmd.Flags().StringVarP(&selector, "selector", "l", "", "label selector")
    podsCmd.Flags().BoolVarP(&watch, "watch", "w", false, "watch for changes")
}

func listPods(namespace string, allNamespaces bool, selector string, output string) error {
    // Implementation
    fmt.Printf("Listing pods (namespace: %s, all: %v, selector: %s, format: %s)\n",
        namespace, allNamespaces, selector, output)
    return nil
}

func getPod(namespace, name, output string) error {
    // Implementation
    fmt.Printf("Getting pod: %s (namespace: %s, format: %s)\n", name, namespace, output)
    return nil
}

Create Deployment (cmd/create/deployment.go)

package create

import (
    "fmt"

    "github.com/spf13/cobra"
)

var (
    image    string
    replicas int
    port     int
)

var deploymentCmd = &cobra.Command{
    Use:   "deployment NAME",
    Short: "Create a deployment",
    Args:  cobra.ExactArgs(1),
    RunE: func(cmd *cobra.Command, args []string) error {
        name := args[0]

        if image == "" {
            return fmt.Errorf("--image is required")
        }

        fmt.Printf("Creating deployment: %s\n", name)
        fmt.Printf("  Image: %s\n", image)
        fmt.Printf("  Replicas: %d\n", replicas)
        if port > 0 {
            fmt.Printf("  Container Port: %d\n", port)
        }

        return createDeployment(name, image, replicas, port)
    },
}

func init() {
    deploymentCmd.Flags().StringVar(&image, "image", "", "container image (required)")
    deploymentCmd.Flags().IntVar(&replicas, "replicas", 1, "number of replicas")
    deploymentCmd.Flags().IntVar(&port, "port", 0, "container port")

    deploymentCmd.MarkFlagRequired("image")
}

func createDeployment(name, image string, replicas, port int) error {
    // Implementation
    return nil
}

Best Practices

1. Consistent Flag Naming

  • Use single-letter shortcuts for common flags (-n, -o, -v)
  • Use descriptive long names (--namespace, --output, --verbose)
  • Keep flag behavior consistent across commands

2. Dynamic Completion

Provide shell completion for resource names:

ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
    return client.ListResourceNames(), cobra.ShellCompDirectiveNoFileComp
}

3. Error Messages

Provide helpful error messages with suggestions:

if image == "" {
    return fmt.Errorf("--image is required. Example: --image nginx:latest")
}

4. Dry Run Support

Support --dry-run for preview:

if dryRun {
    fmt.Printf("Would create deployment: %s\n", name)
    return nil
}

5. Output Formats

Support multiple output formats:

switch output {
case "json":
    return printJSON(pods)
case "yaml":
    return printYAML(pods)
default:
    return printTable(pods)
}

Testing

func TestGetPodsCommand(t *testing.T) {
    cmd := get.GetCmd
    cmd.SetArgs([]string{"pods", "--namespace", "production"})

    err := cmd.Execute()
    if err != nil {
        t.Errorf("Expected no error, got %v", err)
    }
}

Advanced Features

1. Watch Mode

if watch {
    return watchPods(namespace, selector)
}

2. Label Selectors

podsCmd.Flags().StringVarP(&selector, "selector", "l", "", "label selector (e.g., app=nginx)")

3. Field Selectors

podsCmd.Flags().StringVar(&fieldSelector, "field-selector", "", "field selector (e.g., status.phase=Running)")

4. Multiple Output Formats

podsCmd.Flags().StringVarP(&output, "output", "o", "text", "output format (text|json|yaml|wide)")

This example provides a complete kubectl-style CLI structure that you can adapt for your resource management needs.