diff --git a/plugins/kubectl/driver/kubectl.driver.zsh b/plugins/kubectl/driver/kubectl.driver.zsh index 68a080b..acb7b0a 100644 --- a/plugins/kubectl/driver/kubectl.driver.zsh +++ b/plugins/kubectl/driver/kubectl.driver.zsh @@ -3,6 +3,7 @@ unalias k h >/dev/null 2>&1 k() { _SCWRYPTS_KUBECTL_DRIVER kubectl $@; } h() { _SCWRYPTS_KUBECTL_DRIVER helm $@; } +f() { _SCWRYPTS_KUBECTL_DRIVER flux $@; } _SCWRYPTS_KUBECTL_DRIVER() { @@ -57,7 +58,7 @@ _SCWRYPTS_KUBECTL_DRIVER() { " local USAGE__description=" - Provides 'k' (kubectl) and 'h' (helm) shorthands to the respective + Provides 'k' (kubectl), 'h' (helm), and 'f' (flux) shorthands to the respective utility. These functions leverage redis and scwrypts environments to allow quick selection of contexts and namespaces usable across all active shell instances. @@ -100,14 +101,12 @@ _SCWRYPTS_KUBECTL_DRIVER() { --subsession ) SUBSESSION=$2; shift 1 ;; -n | --namespace ) - echo "TODO: set namespace ('$2')" >&2 - USER_ARGS+=(--namespace $2); shift 1 + _SCWRYPTS_KUBECTL_DRIVER kubectl meta set namespace $2 + shift 1 ;; -k | --context | --kube-context ) - echo "TODO: set context ('$2')" >&2 - [[ $CLI =~ ^helm$ ]] && USER_ARGS+=(--kube-context $2) - [[ $CLI =~ ^kubectl$ ]] && USER_ARGS+=(--context $2) + _SCWRYPTS_KUBECTL_DRIVER kubectl meta set context $2 shift 1 ;; @@ -149,6 +148,7 @@ _SCWRYPTS_KUBECTL_DRIVER() { [ $CONTEXT ] && [[ $CLI =~ ^helm$ ]] && CLI_ARGS+=(--kube-context $CONTEXT) [ $CONTEXT ] && [[ $CLI =~ ^kubectl$ ]] && CLI_ARGS+=(--context $CONTEXT) + [ $CONTEXT ] && [[ $CLI =~ ^flux$ ]] && CLI_ARGS+=(--context $CONTEXT) [[ $STRICT -eq 1 ]] && { [ $CONTEXT ] || ERROR "missing kubectl 'context'" diff --git a/plugins/kubectl/driver/meta.zsh b/plugins/kubectl/driver/meta.zsh index aeb4ef2..9b0c021 100644 --- a/plugins/kubectl/driver/meta.zsh +++ b/plugins/kubectl/driver/meta.zsh @@ -30,8 +30,15 @@ SCWRYPTS_KUBECTL_CUSTOM_COMMAND_PARSE__meta() { USAGE__args="set (namespace|context)" USAGE__description="interactively set a namespace or context for '$SCWRYPTS_ENV'" case $2 in - namespace | context ) USER_ARGS+=($1 $2) ;; + namespace | context ) USER_ARGS+=($1 $2 $3); [ $3 ] && shift 1 ;; -h | --help ) HELP=1 ;; + '' ) + : \ + && SCWRYPTS_KUBECTL_CUSTOM_COMMAND__meta set context \ + && SCWRYPTS_KUBECTL_CUSTOM_COMMAND__meta set namespace \ + ; + return $? + ;; * ) ERROR "cannot set '$2'" ;; esac @@ -94,7 +101,7 @@ SCWRYPTS_KUBECTL_CUSTOM_COMMAND__meta() { ;; set ) - scwrypts -n --name set-$2 --type zsh --group kubectl -- --subsession $SUBSESSION >/dev/null \ + scwrypts -n --name set-$2 --type zsh --group kubectl -- $3 --subsession $SUBSESSION >/dev/null \ && SUCCESS "$2 set" ;; diff --git a/plugins/kubectl/lib/kubectl.module.zsh b/plugins/kubectl/lib/kubectl.module.zsh index b2c960a..3456452 100644 --- a/plugins/kubectl/lib/kubectl.module.zsh +++ b/plugins/kubectl/lib/kubectl.module.zsh @@ -21,7 +21,6 @@ KUBECTL() { kubectl ${KUBECTL_ARGS[@]} $@ } - ##################################################################### KUBECTL__GET_CONTEXT() { REDIS get --prefix "current:context"; } @@ -87,3 +86,73 @@ KUBECTL__LIST_NAMESPACES() { echo default KUBECTL get namespaces -o name | sed 's/^namespace\///' | sort } + +##################################################################### + +KUBECTL__SERVE() { + [ $CONTEXT ] || local CONTEXT=$(KUBECTL__GET_CONTEXT) + [ $CONTEXT ] || ERROR 'must configure a context in which to serve' + + [ $NAMESPACE ] || local NAMESPACE=$(KUBECTL__GET_NAMESPACE) + [ $NAMESPACE ] || ERROR 'must configure a namespace in which to serve' + + CHECK_ERRORS --no-fail --no-usage || return 1 + + [ $SERVICE ] && SERVICE=$(KUBECTL__LIST_SERVICES | jq -c "select (.service == \"$SERVICE\")" || echo $SERVICE) + [ $SERVICE ] || local SERVICE=$(KUBECTL__SELECT_SERVICE) + [ $SERVICE ] || ERROR 'must provide or select a service' + + KUBECTL__LIST_SERVICES | grep -q "^$SERVICE$"\ + || ERROR "no service '$SERVICE' in '$CONFIG/$NAMESPACE'" + + CHECK_ERRORS --no-fail --no-usage || return 1 + + ########################################## + + SERVICE_PASSWORD="$(KUBECTL__GET_SERVICE_PASSWORD)" + KUBECTL__SERVICE_PARSE + + INFO "attempting to serve ${NAMESPACE}/${SERVICE_NAME}:${SERVICE_PORT}" + [ $SERVICE_PASSWORD ] && INFO "password : $SERVICE_PASSWORD" + + KUBECTL port-forward service/$SERVICE_NAME $SERVICE_PORT +} + +KUBECTL__SELECT_SERVICE() { + [ $NAMESPACE ] || local NAMESPACE=$(KUBECTL__GET_NAMESPACE) + [ $NAMESPACE ] || return 1 + + local SERVICES=$(KUBECTL__LIST_SERVICES) + local SELECTED=$({ + echo "namespace service port" + echo $SERVICES \ + | jq -r '.service + " " + .port' \ + | sed "s/^/$NAMESPACE /" \ + ; + } \ + | column -t \ + | FZF 'select a service' --header-lines=1 \ + | awk '{print $2;}' \ + ) + + echo $SERVICES | jq -c "select (.service == \"$SELECTED\")" +} + +KUBECTL__LIST_SERVICES() { + KUBECTL get service --no-headers\ + | awk '{print "{\"service\":\""$1"\",\"ip\":\""$3"\",\"port\":\""$5"\"}"}' \ + | jq -c 'select (.ip != "None")' \ + ; +} + +KUBECTL__GET_SERVICE_PASSWORD() { + [ $PASSWORD_SECRET ] && [ $PASSWORD_KEY ] || return 0 + + KUBECTL get secret $PASSWORD_SECRET -o jsonpath="{.data.$PASSWORD_KEY}" \ + | base64 --decode +} + +KUBECTL__SERVICE_PARSE() { + SERVICE_NAME=$(echo $SERVICE | jq -r .service) + SERVICE_PORT=$(echo $SERVICE | jq -r .port | sed 's|/.*$||') +} diff --git a/plugins/kubectl/serve b/plugins/kubectl/serve new file mode 100755 index 0000000..03b6915 --- /dev/null +++ b/plugins/kubectl/serve @@ -0,0 +1,58 @@ +#!/bin/zsh +##################################################################### +DEPENDENCIES+=() +REQUIRED_ENV+=() + +use kubectl --group kubectl + +CHECK_ENVIRONMENT +##################################################################### + +MAIN() { + local USAGE=" + usage: [service] [...options...] + + args: + service (optional) name of the service to forward locally + + options: + --context override context + --namespace override namespace + --subsession REDIS subsession (default 0) + + to show a required password on screen, use both: + --password-secret Secret resource + --password-key key within Secret's 'data' + + -h, --help show this dialogue and exit + " + local CONTEXT NAMESPACE SERVICE + local SUBSESSION=0 + + while [[ $# -gt 0 ]] + do + case $1 in + --context ) CONTEXT=$2; shift 1 ;; + --namespace ) NAMESPACE=$2; shift 1 ;; + --subsession ) SUBSESSION=$2; shift 1 ;; + + --password-secret ) PASSWORD_SECRET=$2; shift 1 ;; + --password-key ) PASSWORD_KEY=$2; shift 1 ;; + + -h | --help ) USAGE; return 0 ;; + + * ) + [ $SERVICE ] && ERROR "unexpected argument '$2'" + SERVICE=$1 + ;; + esac + shift 1 + done + + CHECK_ERRORS + + KUBECTL__SERVE +} + +##################################################################### +MAIN $@ diff --git a/plugins/kubectl/set-context b/plugins/kubectl/set-context index 485e8eb..66391fd 100755 --- a/plugins/kubectl/set-context +++ b/plugins/kubectl/set-context @@ -17,6 +17,8 @@ MAIN() { options: --subsession REDIS subsession (default 0) + + -h, --help show this dialogue and exit " local CONTEXT local SUBSESSION=0 @@ -26,6 +28,8 @@ MAIN() { case $1 in --subsession ) SUBSESSION=$2; shift 1 ;; + -h | --help ) USAGE; return 0 ;; + * ) [ $CONTEXT ] && ERROR "unexpected argument '$2'" CONTEXT=$1 diff --git a/plugins/kubectl/set-namespace b/plugins/kubectl/set-namespace index 1665955..d0363ac 100755 --- a/plugins/kubectl/set-namespace +++ b/plugins/kubectl/set-namespace @@ -17,6 +17,8 @@ MAIN() { options: --subsession REDIS subsession (default 0) + + -h, --help show this dialogue and exit " local NAMESPACE local SUBSESSION=0 @@ -26,6 +28,8 @@ MAIN() { case $1 in --subsession ) SUBSESSION=$2; shift 1 ;; + -h | --help ) USAGE; return 0 ;; + * ) [ $NAMESPACE ] && ERROR "unexpected argument '$2'" NAMESPACE=$1 diff --git a/run b/run index b175f01..3b99697 100755 --- a/run +++ b/run @@ -11,9 +11,10 @@ __RUN() { OPTIONS -g, --group only use scripts from the indicated group -t, --type only use scripts of the indicated type - -m, --name only run the script if there is an exact match + -m, --name only run the script if there is an exact match (requires type and group) + -y, --yes auto-accept all [yn] prompts through current scwrypt -e, --env set environment; overwrites SCWRYPTS_ENV -n, --no-log skip logging and run in quiet mode @@ -56,14 +57,22 @@ __RUN() { VARSPLIT=$(echo "$1 " | sed 's/^\(-.\)\(.*\) /\1 -\2/') set -- $(echo " $VARSPLIT ") ${@:2} ;; + -h | --help ) USAGE return 0 ;; + -n | --no-log ) [ ! $SUBSCWRYPT ] && SUBSCWRYPT=0 shift 1 ;; + + -y | --yes ) + export __SCWRYPTS_YES=1 + shift 1 + ;; + -e | --env ) [ ! $2 ] && ERROR "missing value for argument $1" && break [ ! $SUBSCWRYPTS ] \ @@ -135,7 +144,7 @@ __RUN() { local SCWRYPTS_AVAILABLE local POTENTIAL_ERROR="no such scwrypt exists:" - + SCWRYPTS_AVAILABLE=$(SCWRYPTS__GET_AVAILABLE_SCWRYPTS) [ $SEARCH_NAME ] && { diff --git a/zsh/cloud/aws/eks/login b/zsh/cloud/aws/eks/login index 527c4aa..8c1cf14 100755 --- a/zsh/cloud/aws/eks/login +++ b/zsh/cloud/aws/eks/login @@ -7,4 +7,4 @@ use cloud/aws/eks CHECK_ENVIRONMENT ##################################################################### -EKS_CLUSTER_LOGIN $@ +EKS__CLUSTER_LOGIN $@ diff --git a/zsh/lib/cloud/aws/eks.module.zsh b/zsh/lib/cloud/aws/eks.module.zsh index 8c4f195..e4bfc67 100644 --- a/zsh/lib/cloud/aws/eks.module.zsh +++ b/zsh/lib/cloud/aws/eks.module.zsh @@ -1,19 +1,44 @@ ##################################################################### -DEPENDENCIES+=( - kubectl -) - -REQUIRED_ENV+=( - AWS_ACCOUNT - AWS_REGION -) +DEPENDENCIES+=(kubectl yq) +REQUIRED_ENV+=() use cloud/aws/cli ##################################################################### -EKS_CLUSTER_LOGIN() { +EKS__KUBECTL() { EKS kubectl $@; } +EKS__FLUX() { EKS flux $@; } + +##################################################################### + +EKS() { + local USAGE=" + usage: cli [...kubectl args...] + + args: + cli a kubectl-style CLI (e.g. kubectl, helm, flux, etc) + + Allows access to kubernetes CLI commands by configuring environment + to point to a specific cluster. + " + + REQUIRED_ENV=(AWS_REGION AWS_ACCOUNT CLUSTER_NAME) DEPENDENCIES=(kubectl $1) CHECK_ENVIRONMENT || return 1 + + local CONTEXT="arn:aws:eks:${AWS_REGION}:${AWS_ACCOUNT}:cluster/${CLUSTER_NAME}" + + local CONTEXT_ARGS=() + case $1 in + helm ) CONTEXT_ARGS+=(--kube-context $CONTEXT) ;; + * ) CONTEXT_ARGS+=(--context $CONTEXT) ;; + esac + + $1 ${CONTEXT_ARGS[@]} ${@:2} +} + +##################################################################### + +EKS__CLUSTER_LOGIN() { local USAGE=" usage: [...options...] @@ -25,6 +50,7 @@ EKS_CLUSTER_LOGIN() { cluster in EKS. Also creates the kubeconfig entry if it does not already exist. " + REQUIRED_ENV=(AWS_ACCOUNT AWS_REGION) CHECK_ENVIRONMENT || return 1 local CLUSTER_NAME diff --git a/zsh/lib/cloud/aws/eksctl.module.zsh b/zsh/lib/cloud/aws/eksctl.module.zsh new file mode 100644 index 0000000..5598f0e --- /dev/null +++ b/zsh/lib/cloud/aws/eksctl.module.zsh @@ -0,0 +1,116 @@ +##################################################################### + +DEPENDENCIES+=(eksctl) +REQUIRED_ENV+=() + +use cloud/aws/eks + +##################################################################### + +EKSCTL() { + REQUIRED_ENV=(AWS_PROFILE AWS_REGION) CHECK_ENVIRONMENT || return 1 + + AWS_PROFILE=$AWS_PROFILE AWS_REGION=$AWS_REGION \ + eksctl $@ +} + +EKSCTL__CREATE_IAMSERVICEACCOUNT() { + local USAGE=" + usage: serviceaccount-name namespace [...options...] -- [...'eksctl create iamserviceaccount' args...] + + options: + --serviceaccount (required) target k8s:ServiceAccount + --namespace (required) target k8s:Namespace + --role-name (required) name of the IAM role to assign + + --force don't check for existing serviceaccount and override any existing configuration + + eksctl create iamserviceaccount args: + $(eksctl create iamserviceaccount --help 2>&1 | grep -v -- '--name' | grep -v -- '--namespace' | grep -v -- '--role-name' | sed 's/^/ /') + " + REQUIRED_ENV=(AWS_REGION AWS_ACCOUNT CLUSTER_NAME) CHECK_ENVIRONMENT || return 1 + + local SERVICEACCOUNT NAMESPACE ROLE_NAME + local FORCE=0 + local EKSCTL_ARGS=() + + while [[ $# -gt 0 ]] + do + case $1 in + --serviceaccount ) SERVICEACCOUNT=$2; shift 1 ;; + --namespace ) NAMESPACE=$2; shift 1 ;; + --role-name ) ROLE_NAME=$2; shift 1 ;; + + --force ) FORCE=1 ;; + + -- ) shift 1; break ;; + + * ) ERROR "unknown argument '$1'" ;; + esac + shift 1 + done + + while [[ $# -gt 0 ]]; do EKSCTL_ARGS+=($1); shift 1; done + + [ $SERVICEACCOUNT ] || ERROR "--serviceaccount is required" + [ $NAMESPACE ] || ERROR "--namespace is required" + [ $ROLE_NAME ] || ERROR "--role-name is required" + + CHECK_ERRORS --no-fail || return 1 + + ########################################## + + [[ $FORCE -eq 0 ]] && { + _EKS__CHECK_IAMSERVICEACCOUNT_EXISTS + local EXISTS_STATUS=$? + case $EXISTS_STATUS in + 0 ) + SUCCESS "'$NAMESPACE/$SERVICEACCOUNT' already configured with '$ROLE_NAME'" + return 0 + ;; + 1 ) ;; # role does not exist yet; continue with rollout + 2 ) + ERROR "'$NAMESPACE/$SERVICEACCOUNT' has been configured with a different role than '$ROLE_NAME'" + REMINDER "must use --force flag to overwrite" + return 2 + ;; + esac + } + + STATUS "creating iamserviceaccount" \ + && EKSCTL create iamserviceaccount \ + --cluster $CLUSTER_NAME \ + --namespace $NAMESPACE \ + --name $SERVICEACCOUNT \ + --role-name $ROLE_NAME \ + --override-existing-serviceaccounts \ + --approve \ + ${EKSCTL_ARGS[@]} \ + && SUCCESS "successfully configured '$NAMESPACE/$SERVICEACCOUNT' with IAM role '$ROLE_NAME'" \ + || { ERROR "unable to configure '$NAMESPACE/$SERVICEACCOUNT' with IAM role '$ROLE_NAME' (check cloudformation dashboard for details)"; return 3; } +} + +_EKS__CHECK_IAMSERVICEACCOUNT_EXISTS() { + STATUS "checking for existing role-arn" + local CURRENT_ROLE_ARN=$( + EKS__KUBECTL --namespace $NAMESPACE get serviceaccount $SERVICEACCOUNT -o yaml \ + | YQ -r '.metadata.annotations["eks.amazonaws.com/role-arn"]' \ + | grep -v '^null$' \ + ) + + [ $CURRENT_ROLE_ARN ] || { + STATUS "serviceaccount does not exist or has no configured role" + return 1 + } + + [[ $CURRENT_ROLE_ARN =~ "$ROLE_NAME$" ]] || { + STATUS "serviceaccount current role does not match desired role: + CURRENT : $CURRENT_ROLE_ARN + DESIRED : arn:aws:iam::${AWS_ACCOUNT}:role/$ROLE_NAME + " + return 2 + } + + STATUS "serviceaccount current role matches desired role" + return 0 +} diff --git a/zsh/lib/helm/template.module.zsh b/zsh/lib/helm/template.module.zsh index f75863c..9bffa8e 100644 --- a/zsh/lib/helm/template.module.zsh +++ b/zsh/lib/helm/template.module.zsh @@ -65,6 +65,7 @@ HELM__TEMPLATE__GET() { [ ! $TEMPLATE_OUTPUT ] && EXIT_CODE=1 + [[ $RAW -eq 1 ]] && { [ $USE_CHART_ROOT ] && [[ $USE_CHART_ROOT -eq 1 ]] || HELM_ARGS+=(--show-only $(echo $TEMPLATE_FILENAME | sed "s|^$CHART_ROOT/||")) [[ $COLORIZE -eq 1 ]] \ diff --git a/zsh/lib/helm/validate.module.zsh b/zsh/lib/helm/validate.module.zsh index 982b89d..9d3d621 100644 --- a/zsh/lib/helm/validate.module.zsh +++ b/zsh/lib/helm/validate.module.zsh @@ -27,9 +27,14 @@ HELM__VALIDATE() { return 1 } - CHART_NAME=$(yq -r .name "$CHART_ROOT/Chart.yaml") + CHART_NAME=$(YQ -r .name "$CHART_ROOT/Chart.yaml") - [[ $TEMPLATE_FILENAME =~ values*.yaml$ ]] && { + [[ $TEMPLATE_FILENAME =~ values.*.yaml$ ]] && { + HELM_ARGS+=(--values $TEMPLATE_FILENAME) + USE_CHART_ROOT=1 + } + + [[ $TEMPLATE_FILENAME =~ tests/.*.yaml$ ]] && { HELM_ARGS+=(--values $TEMPLATE_FILENAME) USE_CHART_ROOT=1 } @@ -54,9 +59,18 @@ _HELM__GET_CHART_ROOT() { } _HELM__GET_DEFAULT_VALUES_ARGS() { + for F in \ + "$CHART_ROOT/tests/default.yaml" \ + "$CHART_ROOT/values.test.yaml" \ + "$CHART_ROOT/values.yaml" \ + ; + do + [ -f "$F" ] && HELM_ARGS=(--values "$F" $HELM_ARGS) + done + for LOCAL_REPOSITORY in $(\ cat "$CHART_ROOT/Chart.yaml" \ - | yq -r '.dependencies[] | .repository' \ + | YQ -r '.dependencies[] | .repository' \ | grep '^file://' \ | sed 's|file://||' \ ) @@ -67,22 +81,13 @@ _HELM__GET_DEFAULT_VALUES_ARGS() { ; for F in \ - "$LOCAL_REPOSITORY_ROOT/values.yaml" \ - "$LOCAL_REPOSITORY_ROOT/values.test.yaml" \ "$LOCAL_REPOSITORY_ROOT/tests/default.yaml" \ + "$LOCAL_REPOSITORY_ROOT/values.test.yaml" \ + "$LOCAL_REPOSITORY_ROOT/values.yaml" \ ; do - [ -f "$F" ] && HELM_ARGS+=(--values "$F") + [ -f "$F" ] && HELM_ARGS=(--values "$F" $HELM_ARGS) done done - - for F in \ - "$CHART_ROOT/values.yaml" \ - "$CHART_ROOT/values.test.yaml" \ - "$CHART_ROOT/tests/default.yaml" \ - ; - do - [ -f "$F" ] && HELM_ARGS+=(--values "$F") - done } diff --git a/zsh/lib/utils/dependencies.zsh b/zsh/lib/utils/dependencies.zsh index f01d1c8..9075548 100644 --- a/zsh/lib/utils/dependencies.zsh +++ b/zsh/lib/utils/dependencies.zsh @@ -17,6 +17,13 @@ __CHECK_DEPENDENCY() { $E "application '$1' "$([[ $OPTIONAL -eq 1 ]] && echo preferred || echo required)" but not available on PATH $(__CREDITS $1)" return 1 } + + [[ $DEPENDENCY =~ ^yq$ ]] && { + yq --version | grep -q mikefarah \ + || WARNING 'detected kislyuk/yq but mikefarah/yq is preferred (compatibility may vary)' + } + + return 0 } __CHECK_COREUTILS() { @@ -36,7 +43,7 @@ __CHECK_COREUTILS() { done [[ $NON_GNU_DEPENDENCY_COUNT -gt 0 ]] && { - WARNING 'scripts rely on GNU coreutils; functionality may be limited' + WARNING 'scripts rely on GNU coreutils; compatibility may vary' IS_MACOS && REMINDER 'GNU coreutils can be installed and linked through Homebrew' } diff --git a/zsh/lib/utils/io.zsh b/zsh/lib/utils/io.zsh index d5aa19d..ee134f5 100644 --- a/zsh/lib/utils/io.zsh +++ b/zsh/lib/utils/io.zsh @@ -142,6 +142,7 @@ INPUT() { Yn() { PROMPT "$@ [Yn]" [ $CI ] && { echo y; return 0; } + [ $__SCWRYPTS_YES ] && [[ $__SCWRYPTS_YES -eq 1 ]] && { echo y; return 0; } local Yn; READ -k Yn; echo >&2 [[ $Yn =~ [nN] ]] && return 1 || return 0 @@ -150,6 +151,7 @@ Yn() { yN() { PROMPT "$@ [yN]" [ $CI ] && { echo y; return 0; } + [ $__SCWRYPTS_YES ] && [[ $__SCWRYPTS_YES -eq 1 ]] && { echo y; return 0; } local yN; READ -k yN; echo >&2 [[ $yN =~ [yY] ]] && return 0 || return 1 @@ -218,3 +220,12 @@ EDIT() { $EDITOR $@ /dev/tty SUCCESS "finished editing '$1'!" } + +YQ() { + yq --version | grep -q mikefarah || { + yq $@ + return $? + } + + yq eval '... comments=""' | yq $@ +}