DevOps Control Plane CLI (devctl)

A complete CLI tool built with Cobra and Viper that demonstrates subcommands, flag handling, config file loading, environment variable binding, and multiple output formats. It simulates a devctl control plane with deploy and status subcommands.

Project Structure

devctl/
├── main.go
└── .devctl.yaml        # optional config file

Setup

mkdir devctl
cd devctl
go mod init github.com/yourorg/devctl
go get github.com/spf13/cobra@latest
go get github.com/spf13/viper@latest
go get gopkg.in/yaml.v3

Config File (optional)

Create ~/.devctl.yaml or .devctl.yaml in your working directory to set defaults:

env: staging
output: table

Environment variables override the config file. DEVCTL_ENV=production sets the env without passing --env.

main.go

package main

import (
	"encoding/json"
	"fmt"
	"os"
	"text/tabwriter"

	"github.com/spf13/cobra"
	"github.com/spf13/viper"
	"gopkg.in/yaml.v3"
)

type ServiceStatus struct {
	Name     string `json:"name"     yaml:"name"`
	Env      string `json:"env"      yaml:"env"`
	Status   string `json:"status"   yaml:"status"`
	Replicas int    `json:"replicas" yaml:"replicas"`
}

var rootCmd = &cobra.Command{
	Use:   "devctl",
	Short: "DevOps control plane CLI",
	Long:  "devctl manages deployments and checks service status across environments.",
}

var deployCmd = &cobra.Command{
	Use:   "deploy [service]",
	Short: "Deploy a service to the target environment",
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		env := viper.GetString("env")
		image, _ := cmd.Flags().GetString("image")
		if env == "" {
			return fmt.Errorf("env is required")
		}
		fmt.Fprintf(os.Stderr, "Deploying %s to %s (image: %s)\n", args[0], env, image)
		return nil
	},
}

var statusCmd = &cobra.Command{
	Use:   "status",
	Short: "Show service status",
	RunE: func(cmd *cobra.Command, args []string) error {
		services := []ServiceStatus{
			{Name: "api", Env: viper.GetString("env"), Status: "running", Replicas: 3},
			{Name: "worker", Env: viper.GetString("env"), Status: "running", Replicas: 2},
			{Name: "cache", Env: viper.GetString("env"), Status: "degraded", Replicas: 1},
		}
		return printServices(viper.GetString("output"), services)
	},
}

// printServices renders services in the requested format: table (default), json, or yaml.
func printServices(format string, services []ServiceStatus) error {
	switch format {
	case "json":
		enc := json.NewEncoder(os.Stdout)
		enc.SetIndent("", "  ")
		return enc.Encode(services)
	case "yaml":
		return yaml.NewEncoder(os.Stdout).Encode(services)
	default: // table
		w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
		fmt.Fprintln(w, "NAME\tENV\tSTATUS\tREPLICAS")
		for _, s := range services {
			fmt.Fprintf(w, "%s\t%s\t%s\t%d\n", s.Name, s.Env, s.Status, s.Replicas)
		}
		return w.Flush()
	}
}

func init() {
	cobra.OnInitialize(func() {
		viper.SetConfigName(".devctl")
		viper.SetConfigType("yaml")
		viper.AddConfigPath("$HOME")
		viper.AddConfigPath(".")
		viper.SetEnvPrefix("DEVCTL")
		viper.AutomaticEnv()
		viper.ReadInConfig()
	})

	rootCmd.PersistentFlags().StringP("output", "o", "table", "Output format: table|json|yaml")
	rootCmd.PersistentFlags().StringP("env", "e", "staging", "Target environment")
	viper.BindPFlag("output", rootCmd.PersistentFlags().Lookup("output"))
	viper.BindPFlag("env", rootCmd.PersistentFlags().Lookup("env"))

	deployCmd.Flags().String("image", "", "Container image to deploy")
	deployCmd.MarkFlagRequired("image")

	rootCmd.AddCommand(deployCmd)
	rootCmd.AddCommand(statusCmd)
}

func main() {
	if err := rootCmd.Execute(); err != nil {
		os.Exit(1)
	}
}

Running It

# Build
go build -o devctl .

# Deploy to staging (default)
./devctl deploy api --image myregistry/api:v1.2.3
# Deploying api to staging (image: myregistry/api:v1.2.3)

# Deploy to production
./devctl deploy api --image myregistry/api:v1.2.3 --env production
# Deploying api to production (image: myregistry/api:v1.2.3)

# Status as table (default)
./devctl status
# NAME    ENV      STATUS    REPLICAS
# api     staging  running   3
# worker  staging  running   2
# cache   staging  degraded  1

# Status as JSON
./devctl status -o json
# [{"name":"api","env":"staging","status":"running","replicas":3}, ...]

# Status as YAML
./devctl status -o yaml
# - name: api
#   env: staging
#   status: running
#   replicas: 3

# Override env via environment variable
DEVCTL_ENV=production ./devctl status

💻 Run locally

Copy the code above and run it on your machine

© 2026 ByteLearn.dev. Free courses for developers. · Privacy