Skip to content

Instantly share code, notes, and snippets.

@ybart
Last active May 13, 2026 13:15
Show Gist options
  • Select an option

  • Save ybart/165dddffcec8c6eb07455e036629e14c to your computer and use it in GitHub Desktop.

Select an option

Save ybart/165dddffcec8c6eb07455e036629e14c to your computer and use it in GitHub Desktop.
kucli: multi-environment kubectl helper
#compdef kucli
_kucli() {
local config_file
local git_root=$(git rev-parse --show-toplevel 2>/dev/null)
if [[ -n "$git_root" && -f "$git_root/config/kucli.yml" ]]; then
config_file="$git_root/config/kucli.yml"
elif [[ -f "$PWD/config/kucli.yml" ]]; then
config_file="$PWD/config/kucli.yml"
fi
local -a envs
if [[ -n "$config_file" ]]; then
envs=($(ruby -ryaml -e "puts YAML.load_file('$config_file')['environments'].keys" 2>/dev/null))
fi
# If completing the value for -e, always offer environments regardless of subcommand
if [[ "${words[$CURRENT-1]}" == "-e" ]]; then
compadd -a envs
return
fi
# Find the subcommand from already-typed words (skip options and kucli itself)
local cmd
local w
for w in "${words[@]:1}"; do
[[ "$w" != -* ]] && { cmd="$w"; break }
done
# Strip the subcommand from words so _arguments only sees options
if [[ -n "$cmd" ]]; then
local cmd_index
for (( cmd_index=2; cmd_index<=${#words}; cmd_index++ )); do
[[ "${words[$cmd_index]}" == "$cmd" ]] && break
done
words=("${words[1]}" "${words[@]:$cmd_index}")
(( CURRENT -= cmd_index - 1 ))
fi
local state
case "$cmd" in
status)
_arguments \
'-e[Target environment]:environment:->env' \
'--all[Show all environments]' && return
;;
run|deploy)
_arguments \
'-e[Target environment]:environment:->env' && return
;;
*)
_arguments \
':command:(status run deploy help)' \
'-e[Target environment]:environment:->env' \
'--all[Show all environments (status only)]' && return
;;
esac
case $state in
env) compadd -a envs ;;
esac
}
_kucli "$@"
#!/usr/bin/env ruby
require 'optparse'
require 'yaml'
def find_config
git_root = `git rev-parse --show-toplevel 2>/dev/null`.strip
if !git_root.empty?
path = File.join(git_root, 'config', 'kucli.yml')
return path if File.exist?(path)
end
path = File.join(Dir.pwd, 'config', 'kucli.yml')
return path if File.exist?(path)
nil
end
config_path = find_config
abort "No config/kucli.yml found (tried git root and current directory)." unless config_path
raw = YAML.load_file(config_path)
ENVIRONMENTS = raw.fetch('environments').transform_values { |v| v.transform_keys(&:to_sym) }
DEPLOY_URL_PATTERN = raw['deploy_url_pattern']
def find_default_env(command)
ENVIRONMENTS.each do |key, env|
d = env[:default]
next unless d
return key if d == true
return key if Array(d).map(&:to_s).include?(command.to_s)
end
nil
end
def env_default_label(env)
d = env[:default]
return nil unless d
return " [default]" if d == true
" [default: #{Array(d).join(', ')}]"
end
def show_help
first_env = find_default_env('status') || ENVIRONMENTS.keys.first
puts <<~USAGE
Usage: kucli [command] [options]
Commands:
status (default) Show deployment status for the default or all environments
run Exec into a deployment pod
deploy Show the deploy URL for an environment
help Show this help message
Options:
-e <environment> Target a specific environment (overrides default)
--all Show all environments (status only)
Available environments:
#{ENVIRONMENTS.map { |key, env| " #{key.ljust(20)} #{env[:name]}#{env_default_label(env)}" }.join("\n")}
Examples:
kucli
kucli status --all
kucli status -e #{first_env}
kucli run -e #{first_env}
kucli run -e #{first_env} -- rails console
kucli deploy -e #{first_env}
USAGE
end
def resolve_env!(env_key, command)
key = env_key || find_default_env(command)
abort "#{command} requires -e <environment> (no default configured)\nRun `kucli help` for available environments." unless key
ENVIRONMENTS[key] || abort("Unknown environment: #{key}\nRun `kucli help` for available environments.")
end
def status(env_filter: nil, all: false)
envs = if all
ENVIRONMENTS
elsif env_filter
env = ENVIRONMENTS[env_filter]
abort "Unknown environment: #{env_filter}\nRun `kucli help` for available environments." unless env
{ env_filter => env }
else
default_key = find_default_env('status')
default_key ? { default_key => ENVIRONMENTS[default_key] } : ENVIRONMENTS
end
system('git', 'fetch', out: File::NULL, err: File::NULL)
envs.each do |_key, env|
name, context, namespace, deployment = env.values_at(:name, :context, :namespace, :deployment)
image = `kubectl --context #{context} --namespace #{namespace} get deployments.apps #{deployment} -o jsonpath='{.spec.template.spec.containers[0].image}'`
sha = image.split('/').last
commits_behind = `git rev-list #{sha.split(':').last}..origin/master --count 2>/dev/null`.strip
behind_str = case commits_behind
when "" then "unknown"
when "0" then "up-to-date"
else "#{commits_behind} commits behind"
end
tag = `git tag --points-at #{sha.split(':').last} 2>/dev/null`.split("\n").first&.strip
tag_str = tag&.then { |t| " [#{t}]" }
puts "#{name}: #{sha} (#{behind_str})#{tag_str}"
end
end
def run_exec(env_key, extra_args)
env = resolve_env!(env_key, 'run')
context, namespace, deployment = env.values_at(:context, :namespace, :deployment)
command = extra_args.empty? ? ['/bin/bash'] : extra_args
exec('kubectl', '--context', context, '--namespace', namespace,
'exec', '-it', "deploy/#{deployment}", '--', *command)
end
def deploy(env_key)
abort "No deploy_url_pattern set in config/kucli.yml." unless DEPLOY_URL_PATTERN
env = resolve_env!(env_key, 'deploy')
name = env[:name]
url = DEPLOY_URL_PATTERN.gsub(/:(\w+)/) do
key = $1.to_sym
env[key] || abort("deploy_url_pattern references :#{$1} but it is not set for environment '#{env_key}'")
end
puts "#{name}: #{url}"
end
options = {}
parser = OptionParser.new do |opts|
opts.on('-e ENVIRONMENT', 'Target a specific environment') { |e| options[:env] = e }
opts.on('--all', 'Show all environments (status only)') { options[:all] = true }
opts.on('-h', '--help') { show_help; exit }
end
begin
parser.parse!(ARGV)
rescue OptionParser::InvalidOption => e
abort "#{e.message}\nRun `kucli help` for usage."
end
command = ARGV.shift || 'status'
case command
when 'status'
status(env_filter: options[:env], all: options[:all])
when 'run'
run_exec(options[:env], ARGV)
when 'deploy'
deploy(options[:env])
when 'help'
show_help
else
abort "Unknown command: #{command}\nRun `kucli help` for usage."
end

kucli

Multi-environment kubectl helper. Shows deployment status, opens a shell in a pod, or prints the deploy URL — driven by a per-project config file.

Install

# Script
curl -fsSL https://gist.githubusercontent.com/ybart/165dddffcec8c6eb07455e036629e14c/raw/kucli \
  -o ~/.local/bin/kucli && chmod +x ~/.local/bin/kucli

# Zsh completion
mkdir -p ~/.local/share/zsh/site-functions
curl -fsSL https://gist.githubusercontent.com/ybart/165dddffcec8c6eb07455e036629e14c/raw/_kucli \
  -o ~/.local/share/zsh/site-functions/_kucli

Add to ~/.zshrc (before compinit / oh-my-zsh is sourced):

export PATH="$HOME/.local/bin:$PATH"
fpath=(~/.local/share/zsh/site-functions $fpath)

Then reload: source ~/.zshrc

Project setup

Add config/kucli.yml at the root of each project. See kucli-sample-config.yml for the format.

kucli resolves the config from the git root, falling back to the current directory.

# config/kucli.yml
# Place this file at the root of your project (under config/).
# kucli resolves it from the git root, falling back to the current directory.
#
# deploy_url_pattern (optional): URL template for the `deploy` command.
# Use :key placeholders — each is substituted with the matching value from
# the chosen environment. Any key used in the pattern must be present in
# every environment that supports `deploy`.
#
# Environment fields:
# name: Human-readable label shown in output
# context: kubectl context name (from your kubeconfig)
# namespace: Kubernetes namespace
# deployment: Deployment name, used for `run` (exec into pod)
# default: true (default for all commands) or a list of command names.
# First matching environment in file order wins.
# ...any additional keys referenced by deploy_url_pattern
deploy_url_pattern: "https://deploy.example.com/:cluster_id/:namespace/:deployment"
environments:
production:
name: "MyApp - Production"
context: my-cluster
namespace: my-namespace
deployment: my-app
cluster_id: cluster-prod
default:
- status
staging:
name: "MyApp - Staging"
context: my-staging-cluster
namespace: my-staging-namespace
deployment: my-app-staging
cluster_id: cluster-staging
default: true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment