#!/usr/bin/env bash trap ctctl_exit INT # Print current environnement ctctl_current_env(){ if [ -z "${CTCTL_DOMAIN}" ]; then echo "Unknown container domain" ctctl_exit fi echo "Cluster: ${CTCTL_DOMAIN}" if [ -z "${CTCTL_ENV}" ]; then echo "Unknown container environment" ctctl_exit fi echo "Namespace: ${CTCTL_ENV}" } ctctl_check_env() { if [ -n "${CTCTL_DOMAIN}" -a -n "${CTCTL_ENV}" ]; then echo 1 else echo 0 fi } ctctl_load_config(){ if [ -n "${CTCTL_DOMAIN}" -a -n "${CTCTL_ENV}" ]; then # Load env configuration if [ -e ~/.ctctl/${TARGET_DOM}/${CTCTL_ENV}.conf ]; then set -o allexport source ~/.ctctl/${TARGET_DOM}/${CTCTL_ENV}.conf set +o allexport fi # Load post login configuration if [ -e ~/.ctctl/${CTCTL_DOMAIN}/ctctl.local.conf ]; then set -o allexport source ~/.ctctl/${CTCTL_DOMAIN}/ctctl.local.conf set +o allexport fi fi } # Switch to a target environment (either from no current, or from another current env) ctctl_switch_env(){ TARGET_DOM=$1 TARGET_NAMESPACE=$2 if [ -z "${TARGET_DOM}" ]; then TARGET_DOM=$(ctctl_ls_env | fzf --header "Select the container install you want to work on") fi if [ ! -e ~/.ctctl/${TARGET_DOM}/ctctl.conf ]; then echo "Env ${TARGET_DOM} doesn't exist" ctctl_exit fi # Clear any variable for VAR in $(printenv | grep -E '^(CTCTL|CONSUL|VAULT|NOMAD)_.*' | sed -E 's/^([^=]+)=.*/\1/'); do unset ${VAR} done export CTCTL_DOMAIN=${TARGET_DOM} # Load default config set -o allexport source ~/.ctctl/${CTCTL_DOMAIN}/ctctl.conf set +o allexport # Load pre login env configuration if [ -e ~/.ctctl/${CTCTL_DOMAIN}/pre-login.conf ]; then set -o allexport source ~/.ctctl/${CTCTL_DOMAIN}/pre-login.conf set +o allexport fi # Authenticate so we can list Nomad's namespaces ctctl_auth_env if [ -z "${TARGET_NAMESPACE}" ]; then if [ $(ctctl_ls_namespace | wc -w) -eq 1 ]; then TARGET_NAMESPACE=$(ctctl_ls_namespace) else TARGET_NAMESPACE=$(ctctl_ls_namespace | fzf --header "Select the namespace you are working in") fi fi export CTCTL_ENV=${TARGET_NAMESPACE} # TODO : decide if we keep NOMAD_VAR_env export NOMAD_VAR_env=${TARGET_NAMESPACE} export NOMAD_NAMESPACE=${TARGET_NAMESPACE} } # Check if we have a valid token for vault ctctl_check_vault_token(){ vault token lookup > /dev/null 2>&1 if vault token lookup > /dev/null 2>&1; then echo 1 else echo 0 fi } # Check if we have a valid token for consul ctctl_check_consul_token(){ CONSUL_TOKEN_VALID=0 if [ -n "${CONSUL_HTTP_TOKEN}" ]; then consul acl token read -self > /dev/null 2>&1 if [ $? == 0 ]; then echo 1 else echo 0 fi else echo 0 fi } # Check if we have a valid token for nomad ctctl_check_nomad_token(){ if [ -n "${NOMAD_TOKEN}" ]; then nomad acl token self > /dev/null 2>&1 if [ $? == 0 ]; then echo 1 else echo 0 fi else echo 0 fi } # Auth on vault, consul and nomad on the current env ctctl_auth_env(){ if [ -z "${CTCTL_DOMAIN}" ]; then echo "Unknown environment" ctctl_exit fi NEED_LOGIN=1 if [ "$(ctctl_check_vault_token)" != "1" ]; then if [ -e ~/.vault-token ]; then # If VAULT_TOKEN is defined, unset it and try again. This is because we might # have a valid token in ~/.vault-token but an expired token is set in VAULT_TOKEN # and is taking precedence export VAULT_TOKEN=$(cat ~/.vault-token) if [ "$(ctctl_check_vault_token)" != "1" ]; then unset VAULT_TOKEN else NEED_LOGIN=0 fi fi else NEED_LOGIN=0 fi if [ "${NEED_LOGIN}" = "1" ]; then echo "You're not connected on vault. Please enter your account password" local CTCTL_VAULT_AUTH="vault login -field=token ${VAULT_AUTH_CONFIG:--method=ldap username=${CTCTL_USER:-$(whoami | sed -r 's/\@.*//')}}" export VAULT_TOKEN=$(${CTCTL_VAULT_AUTH} || ctctl_exit) echo "Logged on vault successfuly" else echo "Your vault token is valid" vault token renew > /dev/null 2>&1 fi unset NEED_LOGIN VAULT_TOKEN_INFO=$(vault token lookup -format=json) # TODO make the role selection more generic if [ "$(echo $VAULT_TOKEN_INFO | jq '.data.policies | any(. == "admin-policy" or .== "admin")')" == "true" ]; then NOMAD_ROLE=admin CONSUL_ROLE=admin else NOMAD_ROLE=user CONSUL_ROLE=user fi # Root CA vault read -field certificate pki/root/cert/ca > ~/.ctctl/${CTCTL_DOMAIN}/root_ca.crt # Consul certificate # Get/renew cert if required. # Note 1: as the template is using pkiCert, the cert won't be renewed, unless necessary # Note 2: don't pass CONSUL_CLIENT_CERT CONSUL_CLIENT_KEY and CONSUL_CACERT because they would prevent consul-template from starting # to get/renew the cert if they are absent, or expired env -u CONSUL_CLIENT_CERT \ -u CONSUL_CLIENT_KEY \ -u CONSUL_CACERT \ consul-template -config ~/.ctctl/${CTCTL_DOMAIN}/consul/consul-template.hcl -once # Get/renew cert for Nomad now consul-template -config ~/.ctctl/${CTCTL_DOMAIN}/nomad/consul-template.hcl -once # Check if we have a valid nomad token already if [ "$(ctctl_check_nomad_token)" != "1" ]; then echo "Fecthing a Nomad token from vault" NOMAD_CREDS=$(vault read -format=json ${VAULT_PREFIX:-}nomad/creds/${NOMAD_ROLE}) export NOMAD_TOKEN=$(echo -n ${NOMAD_CREDS} | jq -r .data.secret_id) export NOMAD_VAULT_LEASE=$(echo -n ${NOMAD_CREDS} | jq -r .lease_id) unset NOMAD_CREDS else echo "Nomad token is valid, renewing lease" vault lease renew ${NOMAD_VAULT_LEASE} >/dev/null fi # Check if we have a valid consul token already if [ "$(ctctl_check_consul_token)" != "1" ]; then echo "Fetching a Consul token from vault" CONSUL_CREDS=$(vault read -format=json ${VAULT_PREFIX:-}consul/creds/${CONSUL_ROLE}) export CONSUL_HTTP_TOKEN=$(echo -n ${CONSUL_CREDS} | jq -r .data.token) export CONSUL_VAULT_LEASE=$(echo -n ${CONSUL_CREDS} | jq -r .lease_id) unset CONSUL_CREDS else echo "Consul token is valid, renewing lease" vault lease renew ${CONSUL_VAULT_LEASE} >/dev/null fi ctctl_load_config } ctctl_renew_leases(){ # Renew vault token ([ -n "${VAULT_TOKEN}" ] && vault token renew >/dev/null &) ([ -n "${NOMAD_VAULT_LEASE}" ] && vault lease renew ${NOMAD_VAULT_LEASE} >/dev/null &) ([ -n "${CONSUL_VAULT_LEASE}" ] && vault lease renew ${CONSUL_VAULT_LEASE} >/dev/null &) } # Logout from the current env ctctl_logout_env(){ if [ -z "${CTCTL_DOMAIN}" ]; then echo "Unknown environment" ctctl_exit fi echo "Disconecting from ${CTCTL_DOMAIN} environment" vault token revoke -self for VAR in $(printenv | grep -E '^(CTCTL|CONSUL|VAULT|NOMAD|LOKI)_' | sed -E 's/^((CTCTL|CONSUL|VAULT|NOMAD|LOKI)_[^=]+)=.*/\1/'); do unset $VAR done rm -f ~/.vault-token } # List available env ctctl_ls_env(){ find ~/.ctctl/ -name ctctl.conf | xargs dirname | xargs basename -a } # List available namespaces ctctl_ls_namespace(){ nomad namespace list -json | jq -r ".[] | .Name" } # List buildable Docker images ctctl_ls_build_docker_images(){ (for JOB in $(find . -maxdepth 1 \( -name \*.nomad -o -name \*.nomad.hcl \)); do nomad run -output $JOB | jq '.Job.TaskGroups' | jq '.[] | .Tasks' | jq -r '.[] | .Config.image' 2>/dev/null done) | grep -E "${CTCTL_DOCKER_BUILD_REPO_REGEX:-docker-repo.ehtrace.com}" | sort -u } # Load policies for vault, Consul and Nomad ctctl_load_policies(){ if [ "$(ctctl_check_env)" = "0" ]; then echo "Not currently in a valid env. Run ctctl (with no argument) and select your env first" ctctl_exit fi for DIR in ./output .; do if [ -d "${DIR}/vault/policies" ]; then if [ "$(ctctl_check_vault_token)" != "1" ]; then echo "No valid vault token. You have to authenticate first" ctctl_exit fi for PFILE in $(ls ${DIR}/vault/policies/*.hcl 2>/dev/null); do if [ "${DIR}" = "./output" -a -e "$(echo ${PFILE} | sed -E 's|^\./output/|./|')" ]; then echo "Skiping ${PFILE} as $(echo ${PFILE} | sed -E 's|^\./output/|./|') will take precedence" continue elif [ \! -s "${PFILE}" ]; then echo "Skiping empty file ${PFILE}" continue fi PNAME=$(basename ${PFILE} .hcl) echo "Loading vault policy ${PNAME}" ctctl_replace_conf_var ${PFILE} | vault policy write ${PNAME} - done fi if [ -d "${DIR}/consul/policies" ]; then if [ "$(ctctl_check_consul_token)" != "1" ]; then echo "No valid consul token. You have to authenticate first" ctctl_exit fi CONSUL_CUR_POLICIES=$(consul acl policy list -format=json) for PFILE in $(ls ${DIR}/consul/policies/*.hcl 2>/dev/null); do PNAME=$(basename ${PFILE} .hcl) if [ "${DIR}" = "./output" -a -e "$(echo ${PFILE} | sed -E 's|^\./output/|./|')" ]; then echo "Skiping ${FILE} as $(echo ${PFILE} | sed -E 's|^\./output/|./|') will take precedence" continue elif [ \! -s "${PFILE}" ]; then echo "Skiping empty file ${PFILE}" continue fi # Consul do not use the same command to create a new policy and to update an existing one # so we need to detect if the policy already exists if [ "$(echo ${CONSUL_CUR_POLICIES} | jq -r '.[] | select(.Name=='\"${PNAME}\"') | .Name')" == "${PNAME}" ]; then echo "Updating consul policy ${PNAME}" ctctl_replace_conf_var ${PFILE} | consul acl policy update -name=${PNAME} -rules=- else echo "Adding new consul policy ${PNAME}" ctctl_replace_conf_var ${PFILE} | consul acl policy create -name=${PNAME} -rules=- fi done fi if [ -d "${DIR}/nomad/policies" ]; then if [ "$(ctctl_check_nomad_token)" != "1" ]; then echo "No valid nomad token. You have to authenticate first" ctctl_exit fi for PFILE in $(ls ${DIR}/nomad/policies/*.hcl 2>/dev/null); do PNAME=$(basename ${PFILE} .hcl) if [ "${DIR}" = "./output" -a -e "$(echo ${PFILE} | sed -E 's|^\./output/|./|')" ]; then echo "Skiping ${PFILE} as $(echo ${PFILE} | sed -E 's|^\./output/|./|') will take precedence" continue elif [ \! -s "${PFILE}" ]; then echo "Skiping empty file ${PFILE}" continue fi echo "Loading Nomad policy ${PNAME}" ctctl_replace_conf_var ${PFILE} | nomad acl policy apply ${PNAME} - done fi done } # Load consul config ctctl_load_consul_conf(){ for DIR in ./output .; do if [ -d "${DIR}/consul/config" ]; then if [ "$(ctctl_check_consul_token)" != "1" ]; then echo "No valid consul token. You have to authenticate first" ctctl_exit fi # Note : service-defaults should be loaded before the others # but that should be the case for FILE in $(ls ${DIR}/consul/config/*.hcl 2>/dev/null); do if [ "${DIR}" = "./output" -a -e "$(echo ${FILE} | sed -E 's|^\./output/|./|')" ]; then echo "Skiping ${FILE} as $(echo ${FILE} | sed -E 's|^\./output/|./|') will take precedence" continue elif [ \! -s "${FILE}" ]; then echo "Skiping empty file ${FILE}" continue fi echo "Loading consul conf from ${FILE}" TEMP=$(mktemp) ctctl_replace_conf_var ${FILE} > ${TEMP} consul config write ${TEMP} rm -f ${TEMP} done # Support storing consul config in subdir eg consul/config/service-defaults/foo.hcl # Or you can even omit service and use consul/config/defaults/bar.hcl, consul/config/intentions/bar.hcl for KIND in service-defaults service-intentions service-router service-resolver proxy-defaults; do if [ -d ${DIR}/consul/config/${KIND} ]; then for FILE in $(ls ${DIR}/consul/config/${KIND}/*.hcl 2>/dev/null); do if [ "${DIR}" = "./output" -a -e "$(echo ${FILE} | sed -E 's|^\./output/|./|')" ]; then echo "Skiping ${FILE} as $(echo ${FILE} | sed -E 's|^\./output/|./|') will take precedence" continue elif [ \! -s "${FILE}" ]; then echo "Skiping empty file ${FILE}" continue fi echo "Loading consul conf from ${FILE}" TEMP=$(mktemp) ctctl_replace_conf_var ${FILE} > ${TEMP} consul config write ${TEMP} rm -f ${TEMP} done fi done fi done } # Build all images for the current project ctctl_build_required_images(){ for DOCKER_IMAGE in $(ctctl_ls_build_docker_images); do if ! docker manifest inspect ${DOCKER_IMAGE} > /dev/null 2>&1; then ctctl_build_image ${DOCKER_IMAGE} else echo "Image ${DOCKER_IMAGE} already available" fi done } # Build selected images ctctl_build_selected_images(){ local NO_CACHE=$1 for DOCKER_IMAGE in $(ctctl_ls_build_docker_images | fzf -m --header "Select images to build (space to select, then enter)"); do ctctl_build_image "${DOCKER_IMAGE}" ${NO_CACHE} done } # Build a single image ctctl_build_image(){ local DOCKER_IMAGE=$1 local NO_CACHE=$2 echo "Building image ${DOCKER_IMAGE}" # Extract the basename of the image, removing the repo and the tag local IMAGE_NAME=$(echo -n ${DOCKER_IMAGE} | sed -E 's/.+\/([^\/]+):.*/\1/') export DOCKER_IMAGE=${DOCKER_IMAGE} local LATEST=$(echo ${DOCKER_IMAGE} | sed 's/:.*/:latest/') if [ "${NO_CACHE}" != "" ]; then NO_CACHE="--no-cache" else NO_CACHE="" fi local FOUND=0 for DIR in ./images ./output/images; do if [ -d $DIR/${IMAGE_NAME} ]; then DOCKER_BUILDKIT=1 BUILDKIT_PROGRESS=plain docker build ${NO_CACHE} -t ${DOCKER_IMAGE} -t ${LATEST} ${CTCTL_DOCKER_BUILD_OPTS:-} $DIR/${IMAGE_NAME} &&\ docker push ${DOCKER_IMAGE} &&\ docker push ${LATEST} FOUND=1 break fi done if [ "${FOUND}" = "0" ]; then echo "Couldn't find Docker image directory" ctctl_exit fi } # Run all executable in the render.d directory ctctl_handle_render_scripts(){ for DIR in ./output ./; do if [ -d "${DIR}/render.d" ]; then for H in $(find ${DIR}/render.d -type f -o -type l | sort); do if [ -x "${H}" ]; then echo "Running render script ${H}" $H $1 else echo "Skiping render script $H (not executable)" fi done fi done } # Run all executable in the prep.d directory ctctl_handle_prep_scripts(){ for DIR in ./output ./; do if [ -d "${DIR}/prep.d" ]; then for H in $(find ${DIR}/prep.d -type f -o -type l | sort); do if [ -x "${H}" ]; then echo "Running prep script ${H}" $H $1 else echo "Skiping prep script $H (not executable)" fi done fi done } ctctl_add_submodule(){ local NAME=$1 local MODULE=$2 local URL=$3 local BRANCH=$4 local DIR=$5 mkdir -p ${DIR} if [ ! -d ${DIR}/${NAME} ]; then echo "Adding ${MODULE} submodule from ${URL} (branch ${BRANCH})" git submodule add --branch ${BRANCH} --name ${MODULE} --force ${URL} ${DIR}/${NAME} else echo "Updating ${MODULE} submodule from ${URL} (branch ${BRANCH})" # Get the git top-level, then compare with $(pwd) to get the correct relative path of the bundle, # from the git parent root point of view (not from $(pwd)) local GIT_ROOT=$(git rev-parse --show-toplevel) local CURRENT_DIR=$(pwd) local MODULE_REL_DIR=$(echo ${CURRENT_DIR} | sed -E "s|^${GIT_ROOT}/||") git submodule set-branch --branch ${BRANCH} -- ${MODULE_REL_DIR}/${DIR}/${NAME} fi git submodule update --init --recursive --remote --merge ${DIR}/${NAME} } # Update ctctl bundles with git ctctl_update_submodules(){ if [ -e "bundles.yml" ]; then for BUNDLE in $(yq e -o=j -I=0 '.bundles[]' bundles.yml); do local URL=$(echo ${BUNDLE} | jq -r .url) local BRANCH=$(echo ${BUNDLE} | jq -r .branch) local NAME=$(basename ${URL} .git) local MODULE=$(basename $(pwd))_${NAME} # If the branch is not defined, default to master if [ "${BRANCH}" = "null" ]; then BRANCH=master fi ctctl_add_submodule ${NAME} ${MODULE} ${URL} ${BRANCH} bundles if [ -e "bundles/${NAME}/bundles.yml" ]; then for DEP in $(yq e -o=j -I=0 '.dependencies[]' bundles/${NAME}/bundles.yml); do local DEP_URL=$(echo ${DEP} | jq -r .url) local DEP_BRANCH=$(echo ${DEP} | jq -r .branch) local DEP_NAME="${NAME}_$(basename ${DEP_URL} .git)" local DEP_MODULE="${MODULE}_$(basename ${DEP_URL} .git)" # If the branch is not defined, assume the same as the parent bundle if [ "${DEP_BRANCH}" = "null" ]; then DEP_BRANCH=${BRANCH} fi # Handle relative URL for dependencies if echo ${DEP_URL} | grep -qE '^\.\./'; then DEP_URL=$(dirname ${URL})/$(echo ${DEP_URL} | sed -E 's|^\.\./||') fi ctctl_add_submodule ${DEP_NAME} ${DEP_MODULE} ${DEP_URL} ${DEP_BRANCH} bundles done fi done fi } # Render templates using gomplate (or levant for backward compat) ctctl_render_templates(){ # If a bundles.yml file exist, use the new gomplate rendering method if [ -e "bundles.yml" ]; then mkdir -p bundles export CTCTL_BUNDLE_CONFIG=$(mktemp -t tmp.XXXXXXXXX.yml) # First, cleanup any previously rendered files rm -rf output ./*.nomad ./*.nomad.hcl for BUNDLE in $(yq e -o=j -I=0 '.bundles[]' bundles.yml); do local URL=$(echo ${BUNDLE} | jq -r .url) local BRANCH=$(echo ${BUNDLE} | jq -r .branch) local NAME=$(basename ${URL} .git) if [ "${BRANCH}" = "null" ]; then BRANCH=master fi echo "Working on the ${NAME} bundle" if [ ! -d bundles/${NAME} ]; then ctctl_update_submodules fi # Use [[ and ]] so it won't clash with consul-template fragments local GOMPLATE_COMMON_ARGS=(--left-delim '[[' --right-delim ']]') # Setup a vault datasource GOMPLATE_COMMON_ARGS+=(-d vault=vault:///) # And another datasource for the config of the bundle if [ -e "bundles/${NAME}/variables.yml" ]; then GOMPLATE_COMMON_ARGS+=(-d bundle=./bundles/${NAME}/variables.yml) elif [ -e "bundles/${NAME}/variables.yaml" ]; then GOMPLATE_COMMON_ARGS+=(-d bundle=./bundles/${NAME}/variables.yaml) fi # Declare named, external templates if [ -d "bundles/${NAME}/templates" ]; then GOMPLATE_COMMON_ARGS+=(--template ${NAME}=bundles/${NAME}/templates) for SUBFOLDER in $(find bundles/${NAME}/templates -mindepth 1 -maxdepth 2 -type d); do GOMPLATE_COMMON_ARGS+=(--template ${NAME}/$(basename ${SUBFOLDER})=${SUBFOLDER}) done fi if [ -e "bundles/${NAME}/bundles.yml" ]; then for DEP in $(yq e -o=j -I=0 '.dependencies[]' bundles/${NAME}/bundles.yml); do local DEP_URL=$(echo $DEP | jq -r .url) local DEP_NAME="$(basename ${DEP_URL} .git)" if [ -d "bundles/${NAME}_${DEP_NAME}/templates" ]; then GOMPLATE_COMMON_ARGS+=(--template ${DEP_NAME}=bundles/${NAME}_${DEP_NAME}/templates) for SUBFOLDER in $(find bundles/${NAME}_${DEP_NAME}/templates -mindepth 1 -maxdepth 2 -type d); do GOMPLATE_COMMON_ARGS+=(--template ${DEP_NAME}/$(basename ${SUBFOLDER})=${SUBFOLDER}) done fi done fi GOMPLATE_COMMON_ARGS+=(--exclude .git* --exclude ./**/*.swp) local GOMPLATE_BUNDLE_ARGS=(--input-dir "bundles/${NAME}") # Do not render templates from dependencies, variables files, optional files directory content and images (images will be handled later) GOMPLATE_BUNDLE_ARGS+=(--exclude bundles.yml --exclude variables.yml --exclude images/** --exclude templates/** --exclude files/** --exclude example/**) # Now, render the merged config in a temp file # Build a list of configuration file to merge # Files are in order of precedence (firsts win) # Note : if we have a bundle named foobar, and another named anything-foobar-conf # Then consider anything-foobar-conf just provides config to override the foobar one. So put those first in the list # so they will take precedence I=0 local VAR_FILES="" for FILE in ${NAME}.yml \ ${NAME}.yaml \ ${NOMAD_NAMESPACE}.yml \ ${NOMAD_NAMESPACE}.yaml \ variables.yml \ variables.yaml \ ../${NOMAD_NAMESPACE}.yml \ ../${NOMAD_NAMESPACE}.yaml \ ../variables.yml \ ../variables.yaml \ bundles/*-${NAME}-conf/${NAME}.yml \ bundles/*-${NAME}-conf/${NAME}.yaml \ bundles/*-${NAME}-conf/variables.yml \ bundles/*-${NAME}-conf/variables.yaml \ bundles/${NAME}/variables.yml \ bundles/${NAME}/variables.yaml \ bundles/*/${NAME}.yml \ bundles/*/${NAME}.yaml \ bundles/*/variables.yml \ bundles/*/variables.yaml; do if [ -e ${FILE} ]; then if [ $I -eq 0 ]; then VAR_FILES+='.=merge:' else VAR_FILES+='|' fi VAR_FILES+="${FILE}" I=$((I+1)) fi done gomplate "${GOMPLATE_COMMON_ARGS[@]}" --context "${VAR_FILES[@]}" -i "[[ . | toYAML ]]" -o ${CTCTL_BUNDLE_CONFIG} # And render it again so we can replace any templated values in the config itself # Render it as many time as needed until no [[ ]] are found while grep -q '\[\[' ${CTCTL_BUNDLE_CONFIG}; do gomplate "${GOMPLATE_COMMON_ARGS[@]}" --context ${VAR_FILES} -f ${CTCTL_BUNDLE_CONFIG} -o ${CTCTL_BUNDLE_CONFIG} done GOMPLATE_COMMON_ARGS+=(--context .=file://${CTCTL_BUNDLE_CONFIG}) # This is used for two things # - Add the consul.suffix to every files (except job files). This allows ctctl to simply infer the policy name from the file name # - Rename all files by replacing the default .instance with the configured one # - Put job files in the current dir for conveniance, and everything else in the output dir local GOMPLATE_OUT_ARGS=(--output-map) GOMPLATE_OUT_ARGS+=('[[ if (regexp.Match ".*\\.nomad(\\.hcl)?" .in) ]][[ .in | regexp.Replace (ds "bundle").instance .ctx.instance ]][[ else ]]output/[[ .in | path.Dir ]]/[[ .in | path.Base | regexp.Replace (ds "bundle").instance .ctx.instance | regexp.Replace "^([^\\.]+)\\.(.*)$" (printf "%s%s.%s" "$1" .ctx.consul.suffix "$2") ]][[ end ]]') echo "Redering bundles with gomplate ${GOMPLATE_COMMON_ARGS[@]} ${GOMPLATE_BUNDLE_ARGS[@]} ${GOMPLATE_OUT_ARGS[@]}" # Now render the bundle files gomplate "${GOMPLATE_COMMON_ARGS[@]}" "${GOMPLATE_BUNDLE_ARGS[@]}" "${GOMPLATE_OUT_ARGS[@]}" for IMGDIR in $(find . -name images -type d | grep -vE '^(./output|./bundles/.+/example)'); do for DOCKER_IMAGE in $(find ${IMGDIR} -mindepth 1 -maxdepth 1 -type d); do echo "Redering Docker image $(basename ${DOCKER_IMAGE})" gomplate "${GOMPLATE_COMMON_ARGS[@]}" --input-dir ${DOCKER_IMAGE} --exclude resources/** --exclude root/** --output-dir output/images/$(basename ${DOCKER_IMAGE})/ for ROOT in resources root; do if [ -d "${DOCKER_IMAGE}/${ROOT}" ]; then cp -r "${DOCKER_IMAGE}/${ROOT}" output/images/$(basename ${DOCKER_IMAGE})/ fi done done done echo echo "Formating job files" find ./ -maxdepth 1 -type f \( -name \*nomad.hcl -o -name \*.nomad \) -exec nomad fmt {} \; # Run render.d scripts ctctl_handle_render_scripts # And now delete the merged config rm -f ${CTCTL_BUNDLE_CONFIG} if [ "$(echo ${BUNDLE} | jq -r '.render_example // "false"')" = "true" ]; then echo "Rendering example job with bundle variables only" I=0 local VAR_FILES="" for FILE in bundles/${NAME}/variables.yml \ bundles/${NAME}/variables.yaml \ bundles/*/variables.yml \ bundles/*/variables.yaml; do if [ -e ${FILE} ]; then if [ $I -eq 0 ]; then VAR_FILES+='.=merge:' else VAR_FILES+='|' fi VAR_FILES+="${FILE}" I=$((I+1)) fi done gomplate "${GOMPLATE_COMMON_ARGS[@]}" --context "${VAR_FILES[@]}" -i "[[ . | toYAML ]]" -o ${CTCTL_BUNDLE_CONFIG} # And render it again so we can replace any templated values in the config itself while grep -q '\[\[' ${CTCTL_BUNDLE_CONFIG}; do gomplate "${GOMPLATE_COMMON_ARGS[@]}" --context ${VAR_FILES} -f ${CTCTL_BUNDLE_CONFIG} -o ${CTCTL_BUNDLE_CONFIG} done # First, cleanup any previously rendered files rm -rf bundles/${NAME}/example/* # Now render the bundle files gomplate "${GOMPLATE_COMMON_ARGS[@]}" "${GOMPLATE_BUNDLE_ARGS[@]}" --output-dir=bundles/${NAME}/example if [ -d "bundles/${NAME}/images" ]; then for DOCKER_IMAGE in $(find bundles/${NAME}/images -mindepth 1 -maxdepth 1 -type d); do gomplate "${GOMPLATE_COMMON_ARGS[@]}" --input-dir ${DOCKER_IMAGE} --exclude resources/** --exclude root/** --output-dir bundles/${NAME}/example/images/$(basename ${DOCKER_IMAGE})/ for ROOT in resources root; do if [ -d "${DOCKER_IMAGE}/${ROOT}" ]; then cp -r "${DOCKER_IMAGE}/${ROOT}" bundles/${NAME}/example/images/$(basename ${DOCKER_IMAGE})/ fi done done fi # Format example job files find ./bundles/${NAME}/example -maxdepth 1 -type f \( -name \*nomad.hcl -o -name \*.nomad \) -exec nomad fmt {} \; fi done rm -f ${CTCTL_BUNDLE_CONFIG} unset CTCTL_BUNDLE_CONFIG else # backward compatible, levant based rendering MERGED_CONF=$(mktemp tmp.XXXXXXXX.yml) ctctl_get_merged_conf > ${MERGED_CONF} ctctl_handle_render_scripts ${MERGED_CONF} for TEMPLATE in $(find . -type f -name \*.tpl ! -path "*templates/*"); do local DIR=$(dirname ${TEMPLATE}) local FILE=$(basename ${TEMPLATE} .tpl) local DEST=${DIR}/${FILE} echo "Rendering ${TEMPLATE} into ${DEST}" # Note: render twice, so included templates get rendered too levant render -var-file ${MERGED_CONF} -log-level=WARN <(levant render -var-file ${MERGED_CONF} -log-level=WARN ${TEMPLATE}) > ${DEST} nomad fmt ${DEST} done rm -f ${MERGED_CONF} fi } # Print Consul and Nomad tokens (not vault, for security reasons) ctctl_print_tokens(){ if [ "$(ctctl_check_nomad_token)" == "1" ]; then echo "Nomad token: ${NOMAD_TOKEN}" else echo "No valid Nomad token, you should auth with ctctl auth" fi if [ "$(ctctl_check_consul_token)" == "1" ]; then echo "Consul token: ${CONSUL_HTTP_TOKEN}" else echo "No valid Consul token, you should auth with ctctl auth" fi } # Follow current jobs logs ctctl_loki_logs(){ # Remove the first arg passed to ctctl, which is logs shift local SELECTOR local LOGCLI_CMD if [ -z "${LOKI_ADDR}" ]; then echo "You need to configure loki first (LOKI_ADDR, LOKI_USERNAME and LOKI_PASSWORD or LOKI_PWD_CMD)" ctctl_exit fi if [ -n "${LOKI_PWD_CMD}" ]; then export LOKI_PASSWORD=$(eval ${LOKI_PWD_CMD}) fi LOGCLI_CMD="logcli query --include-label=job --include-label=group --include-label=task --include-label=alloc" echo -n "$*" | grep -qP '\{.+\}' >/dev/null 2>&1 # If a logcli filter was given, use it. Else, build one for jobs in the current dir if [ $? == 0 ]; then echo "Running ${LOGCLI_CMD} $@" ${LOGCLI_CMD} $@ else # Exclude connect-proxy logs as it's often not wanted SELECTOR='{job=~"'$(ctctl_ls_jobs | sed -zE 's/\n/|/g' | sed -E 's/\s+//' | sed -E 's/\|$//')'", task!~"'${LOKI_IGNORE_TASKS:-connect-proxy-.+|tls-proxy|metrics-proxy|pgbouncer}'"}' echo "Running ${LOGCLI_CMD} $@ ${SELECTOR}" ${LOGCLI_CMD} $@ "${SELECTOR}" fi unset LOKI_PASSWORD } ### Helpers ### # Merge the configuration files for the current env and return the result (as string) ctctl_get_merged_conf() { CONF_FILES="" if [ -e "./vars/defaults.yml" ]; then CONF_FILES="./vars/defaults.yml" fi if [ -e "jobs/common/vars/${CTCTL_ENV}.yml" ]; then CONF_FILES="${CONF_FILES} jobs/common/vars/${CTCTL_ENV}.yml" fi if [ -e "../common/vars/${CTCTL_ENV}.yml" ]; then CONF_FILES="${CONF_FILES} ../common/vars/${CTCTL_ENV}.yml" fi if [ -e "./vars/${CTCTL_ENV}.yml" ]; then CONF_FILES="${CONF_FILES} ./vars/${CTCTL_ENV}.yml" fi if [ "${CONF_FILES}" != "" ]; then echo "---" yq ea '. as $item ireduce ({}; . * $item | ... comments="")' $CONF_FILES else echo -n "" fi } # Replace ${local.conf.foo} or ${foo} with the value of foo from the various merged configuration files # This is used to have policies (vault, consul, nomad) and config (consul intentions etc.) with variables ctctl_replace_conf_var() { MERGED_CONF=$(mktemp) ctctl_get_merged_conf > $MERGED_CONF RES=$(cat $1 | \ # Replace ${local.conf.foo} or ${foo} with the value of foo from the various merged configuration files \ # This is used to have policies (vault, consul, nomad) and config (consul intentions etc.) with variables \ perl -pe 'sub replace($) { my $val = shift; chomp(my $res = qx(yq .$val '$MERGED_CONF')); return $res; }; s!\$\{(local\.conf\.)?([^\}]+)\}! replace($2) !ge' | \ # Replace $(foo) with the output of foo command, mainly used to fetch secrets from vault \ perl -pe 'sub replace($) { my $val = shift; chomp(my $res = qx($val)); return $res; }; s!\$\(([^\)]+)\)! replace($1) !ge' ) rm -f $MERGED_CONF echo "${RES}" } # Get a value from the conf ctctl_get_conf(){ ctctl_get_merged_conf | yq ".$1" } # Return a space separated list of jobs the current dir ctctl_ls_jobs(){ local JOBS="" if [ $(find . -maxdepth 1 \( -name \*.nomad -o -name \*.nomad.hcl \) | wc -l) -gt 0 ]; then for JOBFILE in $(find . -maxdepth 1 \( -name \*.nomad -o -name \*.nomad.hcl \)); do echo $(nomad run -output ${JOBFILE} | jq -r '.Job.Name') done else # If current dir has no job file, return all running jobs nomad job status -short | grep -E '\s+running\s+' | cut -d' ' -f1 fi unset JOB JOBFILE } # Return a list of allocation for the given job ctctl_ls_alloc_of_job(){ local JOB=$1 local IFS=$'\n' for ALLOC in $(nomad alloc status -json | jq -c ".[] | select(.JobID==\"${JOB}\") | select(.ClientStatus==\"running\")" | sort); do local ID="$(echo ${ALLOC} | jq -r .ID)" local GROUP="$(echo ${ALLOC} | jq -r .TaskGroup)" local ALLOC_INDEX="$(echo ${ALLOC} | jq -r .Name | sed -E "s/.*\[([0-9]+)\].*/\1/")" local HOST="$(echo ${ALLOC} | jq -r .NodeName)" echo "${ID} (Task group ${GROUP}, allocation index ${ALLOC_INDEX} on host ${HOST}" done unset JOB ID GROUP ALLOC_INDEX HOST } # Return a list of tasks for the given allocation ctctl_ls_tasks_of_alloc(){ local ALLOC=$1 local IFS=$'\n' for TASK in $(nomad alloc status -json "${ALLOC}" | jq -r '.TaskStates | to_entries[] | select(.value.State=="running") | select(.key | startswith("connect-proxy") | not) | .key' | sort); do echo "${TASK}" done unset TASK ALLOC } # Exec a command in a container ctctl_exec_ct(){ local IFS=$'\n' local CMD=$1 ALLOCS=$(for JOB in $(ctctl_ls_jobs); do ctctl_ls_alloc_of_job ${JOB}; done) if [ $(echo "${ALLOCS}" | wc -l) -eq 1 ]; then ALLOC="${ALLOCS}" else ALLOC=$(echo "${ALLOCS}" | fzf --header "Select desired allocation") fi ALLOC=$(echo ${ALLOC} | sed -E 's/^([^ \(]+).*/\1/') # Only Keep the UUID of the target alloc ALLOC=$(echo ${ALLOC} | sed -E 's/^([^ \(]+).*/\1/') TASKS=$(ctctl_ls_tasks_of_alloc "${ALLOC}") if [ $(echo "${TASKS}" | wc -l) -eq 1 ]; then TASK=${TASKS} else TASK=$(echo "${TASKS}" | fzf --header "Select desired task") fi echo "Running nomad alloc exec -task ${TASK} ${ALLOC} ${CMD}" nomad alloc exec -task ${TASK} ${ALLOC} ${CMD} unset TASKS TASK ALLOCS ALLOC CMD } # Enter a container by execing sh # This is just a shortcut for exec sh ctctl_enter_ct(){ ctctl_exec_ct sh } # Follow logs of a task ctctl_alloc_logs(){ local IFS=$'\n' ALLOCS=$(for JOB in $(ctctl_ls_jobs); do ctctl_ls_alloc_of_job ${JOB}; done) if [ $(echo "${ALLOCS}" | wc -l) -eq 1 ]; then ALLOC="${ALLOCS}" else ALLOC=$(echo "${ALLOCS}" | fzf --header "Select desired allocation") fi ALLOC=$(echo ${ALLOC} | sed -E 's/^([^ \(]+).*/\1/') TASKS=$(ctctl_ls_tasks_of_alloc ${ALLOC}) if [ $(echo "${TASKS}" | wc -l) -eq 1 ]; then TASK=${TASKS} else TASK=$(echo "${TASKS}" | fzf --header "Select desired task") fi echo "Running nomad alloc logs -f ${ALLOC} ${TASK}" nomad alloc logs -f ${ALLOC} ${TASK} unset ALLOCS ALLOC TASKS TASK } ctctl_exit(){ # Cleanup by unseting all functions for FUNC in $(declare -F | grep -E '^declare -f ctctl_' | sed -E 's/^declare -f //'); do unset -f ${FUNC} done # Remove trap on SIGINT trap - INT kill -INT $$ } export FZF_DEFAULT_OPTS=${CTCTL_FZF_DEFAULT_OPTS:-"--height=~25% --cycle --bind 'space:toggle' --marker='*'"} case $1 in current) ctctl_current_env ctctl_renew_leases ;; auth) ctctl_auth_env ;; disconnect) ctctl_logout_env ;; ls|list) ctctl_ls_env ctctl_renew_leases ;; render) ctctl_render_templates ctctl_renew_leases ;; fetch) ctctl_update_submodules ctctl_renew_leases ;; prep|prepare) ctctl_update_submodules ctctl_render_templates ctctl_handle_prep_scripts ctctl_load_policies ctctl_load_consul_conf ctctl_build_required_images ctctl_renew_leases ;; load-conf) ctctl_load_policies ctctl_load_consul_conf ctctl_renew_leases ;; build) ctctl_build_selected_images ctctl_renew_leases ;; build-no-cache) ctctl_build_selected_images "no-cache" ctctl_renew_leases ;; tokens) ctctl_print_tokens ctctl_renew_leases ;; logs) shift ctctl_alloc_logs "$@" ctctl_renew_leases ;; loki) ctctl_loki_logs "$@" ctctl_renew_leases ;; conf) ctctl_get_merged_conf ctctl_renew_leases ;; exec) shift ctctl_exec_ct "$@" ctctl_renew_leases ;; sh) ctctl_enter_ct ctctl_renew_leases ;; switch) shift ctctl_switch_env "$@" ctctl_auth_env ;; *) ctctl_switch_env "$@" ctctl_auth_env ;; esac ctctl_exit