diff --git a/bin/kubectx b/bin/kubectx new file mode 100755 index 0000000..0a85429 --- /dev/null +++ b/bin/kubectx @@ -0,0 +1,227 @@ +#!/usr/bin/env bash +# +# kubectx(1) is a utility to manage and switch between kubectl contexts. + +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[[ -n $DEBUG ]] && set -x + +set -eou pipefail +IFS=$'\n\t' + +SELF_CMD="$0" +KUBECTX="${XDG_CACHE_HOME:-$HOME/.kube}/kubectx" + +usage() { + cat <<"EOF" +USAGE: + kubectx : list the contexts + kubectx : switch to context + kubectx - : switch to the previous context + kubectx = : rename context to + kubectx =. : rename current-context to + kubectx -d [] : delete context ('.' for current-context) + (this command won't delete the user/cluster entry + that is used by the context) + + kubectx -h,--help : show this message +EOF +} + +exit_err() { + echo >&2 "${1}" + exit 1 +} + +current_context() { + $KUBECTL config view -o=jsonpath='{.current-context}' +} + +get_contexts() { + $KUBECTL config get-contexts -o=name | sort -n +} + +list_contexts() { + set -u pipefail + local cur ctx_list + cur="$(current_context)" || exit_err "error getting current context" + ctx_list=$(get_contexts) || exit_err "error getting context list" + + local yellow darkbg normal + yellow=$(tput setaf 3 || true) + darkbg=$(tput setab 0 || true) + normal=$(tput sgr0 || true) + + local cur_ctx_fg cur_ctx_bg + cur_ctx_fg=${KUBECTX_CURRENT_FGCOLOR:-$yellow} + cur_ctx_bg=${KUBECTX_CURRENT_BGCOLOR:-$darkbg} + + for c in $ctx_list; do + if [[ -n "${_KUBECTX_FORCE_COLOR:-}" || \ + -t 1 && -z "${NO_COLOR:-}" ]]; then + # colored output mode + if [[ "${c}" = "${cur}" ]]; then + echo "${cur_ctx_bg}${cur_ctx_fg}${c}${normal}" + else + echo "${c}" + fi + else + echo "${c}" + fi + done +} + +read_context() { + if [[ -f "${KUBECTX}" ]]; then + cat "${KUBECTX}" + fi +} + +save_context() { + local saved + saved="$(read_context)" + + if [[ "${saved}" != "${1}" ]]; then + printf %s "${1}" > "${KUBECTX}" + fi +} + +switch_context() { + $KUBECTL config use-context "${1}" +} + +choose_context_interactive() { + local choice + choice="$(_KUBECTX_FORCE_COLOR=1 \ + FZF_DEFAULT_COMMAND="${SELF_CMD}" \ + fzf --ansi || true)" + if [[ -z "${choice}" ]]; then + echo 2>&1 "error: you did not choose any of the options" + exit 1 + else + set_context "${choice}" + fi +} + +set_context() { + local prev + prev="$(current_context)" || exit_err "error getting current context" + + switch_context "${1}" + + if [[ "${prev}" != "${1}" ]]; then + save_context "${prev}" + fi +} + +swap_context() { + local ctx + ctx="$(read_context)" + if [[ -z "${ctx}" ]]; then + echo "error: No previous context found." >&2 + exit 1 + fi + set_context "${ctx}" +} + +context_exists() { + grep -q ^"${1}"\$ <($KUBECTL config get-contexts -o=name) +} + +rename_context() { + local old_name="${1}" + local new_name="${2}" + + if [[ "${old_name}" == "." ]]; then + old_name="$(current_context)" + fi + + if ! context_exists "${old_name}"; then + echo "error: Context \"${old_name}\" not found, can't rename it." >&2 + exit 1 + fi + + if context_exists "${new_name}"; then + echo "Context \"${new_name}\" exists, deleting..." >&2 + $KUBECTL config delete-context "${new_name}" 1>/dev/null 2>&1 + fi + + $KUBECTL config rename-context "${old_name}" "${new_name}" +} + +delete_contexts() { + for i in "${@}"; do + delete_context "${i}" + done +} + +delete_context() { + local ctx + ctx="${1}" + if [[ "${ctx}" == "." ]]; then + ctx="$(current_context)" || exit_err "error getting current context" + fi + echo "Deleting context \"${ctx}\"..." >&2 + $KUBECTL config delete-context "${ctx}" +} + +main() { + if hash kubectl 2>/dev/null; then + KUBECTL=kubectl + elif hash kubectl.exe 2>/dev/null; then + KUBECTL=kubectl.exe + else + echo >&2 "kubectl is not installed" + exit 1 + fi + + if [[ "$#" -eq 0 ]]; then + if [[ -t 1 && -z "${KUBECTX_IGNORE_FZF:-}" && "$(type fzf &>/dev/null; echo $?)" -eq 0 ]]; then + choose_context_interactive + else + list_contexts + fi + elif [[ "${1}" == "-d" ]]; then + if [[ "$#" -lt 2 ]]; then + echo "error: missing context NAME" >&2 + usage + exit 1 + fi + delete_contexts "${@:2}" + elif [[ "$#" -gt 1 ]]; then + echo "error: too many arguments" >&2 + usage + exit 1 + elif [[ "$#" -eq 1 ]]; then + if [[ "${1}" == "-" ]]; then + swap_context + elif [[ "${1}" == '-h' || "${1}" == '--help' ]]; then + usage + elif [[ "${1}" =~ ^-(.*) ]]; then + echo "error: unrecognized flag \"${1}\"" >&2 + usage + exit 1 + elif [[ "${1}" =~ (.+)=(.+) ]]; then + rename_context "${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}" + else + set_context "${1}" + fi + else + usage + exit 1 + fi +} + +main "$@" diff --git a/bin/kubectx.completion.bash b/bin/kubectx.completion.bash new file mode 100644 index 0000000..02273c2 --- /dev/null +++ b/bin/kubectx.completion.bash @@ -0,0 +1,8 @@ +_kube_namespaces() +{ + local curr_arg; + curr_arg=${COMP_WORDS[COMP_CWORD]} + COMPREPLY=( $(compgen -W "- $(kubectl get namespaces -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}')" -- $curr_arg ) ); +} + +complete -F _kube_namespaces kubens kns diff --git a/bin/kubens b/bin/kubens new file mode 100755 index 0000000..361e3b3 --- /dev/null +++ b/bin/kubens @@ -0,0 +1,214 @@ +#!/usr/bin/env bash +# +# kubens(1) is a utility to switch between Kubernetes namespaces. + +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[[ -n $DEBUG ]] && set -x + +set -eou pipefail +IFS=$'\n\t' + +SELF_CMD="$0" +KUBENS_DIR="${XDG_CACHE_HOME:-$HOME/.kube}/kubens" + +usage() { + cat <<"EOF" +USAGE: + kubens : list the namespaces in the current context + kubens : change the active namespace of current context + kubens - : switch to the previous namespace in this context + kubens -h,--help : show this message +EOF +} + +exit_err() { + echo >&2 "${1}" + exit 1 +} + +current_namespace() { + local cur_ctx + + cur_ctx="$(current_context)" || exit_err "error getting current context" + ns="$($KUBECTL config view -o=jsonpath="{.contexts[?(@.name==\"${cur_ctx}\")].context.namespace}")" \ + || exit_err "error getting current namespace" + + if [[ -z "${ns}" ]]; then + echo "default" + else + echo "${ns}" + fi +} + +current_context() { + $KUBECTL config current-context +} + +get_namespaces() { + $KUBECTL get namespaces -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}' +} + +escape_context_name() { + echo "${1//\//-}" +} + +namespace_file() { + local ctx="$(escape_context_name "${1}")" + echo "${KUBENS_DIR}/${ctx}" +} + +read_namespace() { + local f + f="$(namespace_file "${1}")" + [[ -f "${f}" ]] && cat "${f}" + return 0 +} + +save_namespace() { + mkdir -p "${KUBENS_DIR}" + local f saved + f="$(namespace_file "${1}")" + saved="$(read_namespace "${1}")" + + if [[ "${saved}" != "${2}" ]]; then + printf %s "${2}" > "${f}" + fi +} + +switch_namespace() { + local ctx="${1}" + $KUBECTL config set-context "${ctx}" --namespace="${2}" + echo "Active namespace is \"${2}\".">&2 +} + +choose_namespace_interactive() { + # directly calling kubens via fzf might fail with a cryptic error like + # "$FZF_DEFAULT_COMMAND failed", so try to see if we can list namespaces + # locally first + if [[ -z "$(list_namespaces)" ]]; then + echo >&2 "error: could not list namespaces (is the cluster accessible?)" + exit 1 + fi + + local choice + choice="$(_KUBECTX_FORCE_COLOR=1 \ + FZF_DEFAULT_COMMAND="${SELF_CMD}" \ + fzf --ansi || true)" + if [[ -z "${choice}" ]]; then + echo 2>&1 "error: you did not choose any of the options" + exit 1 + else + set_namespace "${choice}" + fi +} + +set_namespace() { + local ctx prev + ctx="$(current_context)" || exit_err "error getting current context" + prev="$(current_namespace)" || exit_error "error getting current namespace" + + if grep -q ^"${1}"\$ <(get_namespaces); then + switch_namespace "${ctx}" "${1}" + + if [[ "${prev}" != "${1}" ]]; then + save_namespace "${ctx}" "${prev}" + fi + else + echo "error: no namespace exists with name \"${1}\".">&2 + exit 1 + fi +} + +list_namespaces() { + local yellow darkbg normal + yellow=$(tput setaf 3 || true) + darkbg=$(tput setab 0 || true) + normal=$(tput sgr0 || true) + + local cur_ctx_fg cur_ctx_bg + cur_ctx_fg=${KUBECTX_CURRENT_FGCOLOR:-$yellow} + cur_ctx_bg=${KUBECTX_CURRENT_BGCOLOR:-$darkbg} + + local cur ns_list + cur="$(current_namespace)" || exit_err "error getting current namespace" + ns_list=$(get_namespaces) || exit_err "error getting namespace list" + + for c in $ns_list; do + if [[ -n "${_KUBECTX_FORCE_COLOR:-}" || \ + -t 1 && -z "${NO_COLOR:-}" ]]; then + # colored output mode + if [[ "${c}" = "${cur}" ]]; then + echo "${cur_ctx_bg}${cur_ctx_fg}${c}${normal}" + else + echo "${c}" + fi + else + echo "${c}" + fi + done +} + +swap_namespace() { + local ctx ns + ctx="$(current_context)" || exit_err "error getting current context" + ns="$(read_namespace "${ctx}")" + if [[ -z "${ns}" ]]; then + echo "error: No previous namespace found for current context." >&2 + exit 1 + fi + set_namespace "${ns}" +} + +main() { + if [[ -z "${KUBECTL:-}" ]]; then + if hash kubectl 2>/dev/null; then + KUBECTL=kubectl + elif hash kubectl.exe 2>/dev/null; then + KUBECTL=kubectl.exe + else + echo >&2 "kubectl is not installed" + exit 1 + fi + fi + + if [[ "$#" -eq 0 ]]; then + if [[ -t 1 && -z ${KUBECTX_IGNORE_FZF:-} && "$(type fzf &>/dev/null; echo $?)" -eq 0 ]]; then + choose_namespace_interactive + else + list_namespaces + fi + elif [[ "$#" -eq 1 ]]; then + if [[ "${1}" == '-h' || "${1}" == '--help' ]]; then + usage + elif [[ "${1}" == "-" ]]; then + swap_namespace + elif [[ "${1}" =~ ^-(.*) ]]; then + echo "error: unrecognized flag \"${1}\"" >&2 + usage + exit 1 + elif [[ "${1}" =~ (.+)=(.+) ]]; then + alias_context "${BASH_REMATCH[2]}" "${BASH_REMATCH[1]}" + else + set_namespace "${1}" + fi + else + echo "error: too many flags" >&2 + usage + exit 1 + fi +} + +main "$@" diff --git a/bin/kubens.completion.bash b/bin/kubens.completion.bash new file mode 100644 index 0000000..d8a594b --- /dev/null +++ b/bin/kubens.completion.bash @@ -0,0 +1,8 @@ +_kube_contexts() +{ + local curr_arg; + curr_arg=${COMP_WORDS[COMP_CWORD]} + COMPREPLY=( $(compgen -W "- $(kubectl config get-contexts --output='name')" -- $curr_arg ) ); +} + +complete -F _kube_contexts kubectx kctx