#!/usr/bin/env bash

set -o errexit
set -o nounset
set -o pipefail

checkBashVersion() {
    if (( "${BASH_VERSION%%.*}" < 4 )); then
        echo "This script requires Bash major version 4 or greater."
        echo "Detected version: $BASH_VERSION"
        if [[ ${OSTYPE:-} =~ [Dd]arwin ]]; then
            echo -e "\nOn macOS, you can install bash with Homebrew:"
            echo "brew install bash"
            echo -e "\nThen run:"
            #shellcheck disable=SC2016
            echo '"$(brew --prefix bash)/bin/bash" ./miniziti.bash ...'
        fi
        exit 1;
    fi
}

banner(){

    local profile="${1-}"

    cat <<BANNER
                  __   ___
    |\/| | |\ | |  / |  |  |
    |  | | | \| | /_ |  |  |

BANNER

    if [[ -n "$profile" && "$profile" != "miniziti" ]]; then
        echo -e "   .: $* :.\n"
    fi
}

_usage(){
    if (( $# )); then
        echo -e "\nERROR: unexpected arg '$1'" >&2
    else
        banner
    fi
    echo -e "\n Basic Commands:\n"\
            "   start\t\tstart miniziti\n"\
            "   delete\t\tdelete miniziti\n"\
            "   console\t\topen ziti-console in browser\n"\
            "   creds\t\tprints admin user updb credentials\n"\
            "   status\t\tprint ziti component status\n"\
            "   ziti\t\tziti cli wrapper\n"\
            "   help\t\tshow these usage hints\n"\
            "\n Profile Commands:\n"\
            "   profile list\tlist available profiles\n"\
            "   profile show\tshow default profile\n"\
            "   profile use\t\tset default profile\n"\
            "\n Advanced Commands:\n"\
            "   shell\t\trun interactive shell inside the ziti-controller container\n"\
            "   login\t\trun local ziti binary edge login\n"\
            "\n Other Commands:\n"\
            "   kubectl\t\tkubectl cli wrapper\n"\
            "   minikube\t\tminikube cli wrapper\n"\
            "\n Options:\n"\
            "   --quiet\t\tsuppress INFO messages\n"\
            "   --verbose\t\tshow DEBUG messages\n"\
            "   --profile\t\tMINIKUBE_PROFILE (miniziti)\n"\
            "   --namespace\t\tZITI_NAMESPACE (MINIKUBE_PROFILE)\n"\
            "   --no-hosts\t\tdon't use local hosts DB or ingress-dns nameserver\n"\
            "   --modify-hosts\tadd entries to local hosts database. Requires sudo if not running as root. Linux only.\n"\
            "\n Debug:\n"\
            "   --charts\t\tZITI_CHARTS_REF (openziti) alternative charts repo\n"\
            "   --values-dir\tEXTRA_VALUES_DIR with Helm values files named ziti-controller.yaml or ziti-router.yaml\n"\
            "   --now\t\teliminate safety waits, e.g., before deleting miniziti\n"\
            "   --\t\t\tMINIKUBE_START_ARGS args after -- passed to minikube start\n"
}

_usageProfileUse(){
    echo -e "\nSet miniziti default profile\n"\
            "\n Usage:\n"\
            "   ziti profile use [profile]\n"
}

_usageProfile(){
    if (( $# )); then
        echo -e "\nERROR: unexpected arg '$1'" >&2
    fi

    echo -e "\nProfile management commands\n"\
            "\n Usage:\n"\
            "   ziti profile [command]\n"\
            "\n Available Commands:\n"\
            "   list\tlist available profiles\n"\
            "   show\tshow default profile\n"\
            "   use\t\tset miniziti profile\n"
}

checkDns(){
    # only param is an IPv4
    [[ $# -eq 1 && $1 =~ ([0-9]{1,3}\.){3}[0-9]{1,3} ]] || {
        logError "need IPv4 of miniziti ingress as only param to checkDns()"
        return 1
    }
    logDebug "checking dns to ensure miniziti-controller.${MINIZITI_INGRESS_ZONE} resolves to $1"
    if grep -qE "^${1//./\\.}.*miniziti-controller.${MINIZITI_INGRESS_ZONE}" /etc/hosts \
        || nslookup "miniziti-controller.${MINIZITI_INGRESS_ZONE}" | grep -q "${1//./\\.}"; then
        logDebug "host dns found expected minikube ingress IP '$1'"
        return 0
    else
        logError "miniziti-controller.${MINIZITI_INGRESS_ZONE} does not resolve to '$1'. Did you add the record in /etc/hosts?"
        return 1
    fi
}

deleteMiniziti(){
    local WAIT=30
    if [[ $1 =~ ^[0-9]+$ ]]; then
        WAIT="$1"
        shift
    else
        logDebug "no integer param detected to deleteMiniziti(), using default wait time ${WAIT}s"
    fi
    (( SAFETY_WAIT )) && {
        logWarn "deleting ${MINIKUBE_PROFILE} in ${WAIT}s, use --now to skip wait" >&2
        sleep "$WAIT"
    }
    logInfo "waiting for ${MINIKUBE_PROFILE} to be deleted"
    minikube --profile "${MINIKUBE_PROFILE}" delete >&3
}

detectOs(){
    if grep -qi "microsoft" /proc/sys/kernel/osrelease 2>/dev/null; then
        logDebug "detected Windows OS"
        echo "Windows"
    elif [[ ${OSTYPE:-} =~ [Dd]arwin ]]; then
        logDebug "detected macOS OS"
        echo "macOS"
    elif [[ ${OSTYPE:-} =~ [Ll]inux ]]; then
        logDebug "detected Linux OS"
        echo "Linux"
    else
        logError "failed to detect OS"
        return 1
    fi
}

makeMinizitiStateDir() {
    case "$DETECTED_OS" in
        macOS)
            state_dir="${XDG_STATE_DIR:-$HOME/Library/Application Support}/miniziti"
            ;;
        Linux|Windows)
            state_dir="${XDG_STATE_DIR:-$HOME/.local/state}/miniziti"
            ;;
        *)
            logError "Unknown os: $DETECTED_OS"
            exit 1
    esac

    if [[ ! -d "$state_dir" ]]; then
        logDebug "Creating miniziti state directory: $state_dir"
        mkdir -p "$state_dir"
    fi

    echo "$state_dir"
}

getZitiCliHome() {
    case "$DETECTED_OS" in
        Linux) echo "$HOME/.config/ziti" ;;
        *) echo "$HOME/.ziti" ;;
    esac
}

pathToNative() {
    local path="$1"
    case "$DETECTED_OS" in
        Windows) wslpath -w "$path" ;;
        *) echo "$path" ;;
    esac
}

testClusterDns(){
    if kubectlWrapper run "dnstest" --rm --tty --stdin --image busybox --restart Never -- \
        nslookup "miniziti-controller.${MINIZITI_INGRESS_ZONE}" | grep "$1" >&3; then
        logInfo "cluster dns test succeeded"
    else
        logError "cluster dns test failed"
        return 1
    fi
}

validateDnsName(){
    if ! [[ $# -eq 1 ]]; then
        logError "validateDnsName() takes one string param"
        return 1
    fi
    if grep -qE '^[a-zA-Z0-9]([-a-zA-Z0-9]{2,251})[a-zA-Z0-9]$' <<<"$1"; then
        logDebug "'$1' is a valid DNS name"
        return 0
    else
        logError "'$1' could not be validated as an unqualified DNS name which is limited to at least four alphanumeric and hyphen characters and does not start nor end with a hyphen."
        return 1
    fi
}

checkSudoRequired() {
    if (( EUID != 0 )); then
        logInfo "sudo is required when not running as root"
    fi
}

HOSTS_FILE='/etc/hosts'

cleanHosts() {
    local marker="$1"
    logWarn "Removing stale miniziti entries from $HOSTS_FILE"
    if (( EUID != 0 )); then
        sudo sed -i "/$marker/d" "$HOSTS_FILE"
    else
        sed -i "/$marker/d" "$HOSTS_FILE"
    fi
}

installHosts() {
    hosts=(
        "miniziti-controller.$MINIZITI_INGRESS_ZONE"
        "miniziti-router.$MINIZITI_INGRESS_ZONE"
    )

    hosts_line="$MINIKUBE_NODE_EXTERNAL ${hosts[*]}"

    if ! grep -q "$hosts_line" "$HOSTS_FILE"; then

        checkSudoRequired

        if grep -q "$MINIZITI_INGRESS_ZONE" "$HOSTS_FILE"; then
            cleanHosts "$MINIZITI_INGRESS_ZONE"
        fi

        logInfo "Adding miniziti entries to $HOSTS_FILE"
        if (( EUID != 0 )); then
            echo "$hosts_line" | sudo tee -a "$HOSTS_FILE" > /dev/null
        else
            echo "$hosts_line" | tee -a "$HOSTS_FILE" > /dev/null
        fi
    fi
}

getAdminSecret() {
    kubectlWrapper get secrets "ziti-controller-admin-secret" \
        --namespace "$ZITI_NAMESPACE" \
        --output go-template='{{index .data "admin-password" | base64decode }}'
}

showAdminCreds() {
    logInfo "The password for 'admin' is: '$(getAdminSecret)'"
}

getConfigMapKey() {
    local configmap="$1"
    local key="$2"
    kubectlWrapper get configmap "$configmap" \
        --ignore-not-found=true \
        --output jsonpath="{.data.$key}" 2> /dev/null
}

getIngressZone() {
    ingress_zone="$(getConfigMapKey "$MINIZITI_CONFIGMAP" 'ingress-zone')"
    if [[ -z "$ingress_zone" ]]; then
        logError "Failed to retrieve ingress zone. Did you start profile '$MINIKUBE_PROFILE' successfully?"
        exit 1
    else
        echo "$ingress_zone"
    fi
}

minizitiLogin() {
    checkCommand ziti
    ingress_zone="$(getIngressZone)"
    getAdminSecret | xargs ziti edge login "https://miniziti-controller.$ingress_zone:443/edge/management/v1" \
            --cli-identity "$MINIKUBE_PROFILE" \
            --yes \
            --username "admin" \
            --password

    logInfo "Setting default ziti identity to: $MINIKUBE_PROFILE"
    ziti edge use "$MINIKUBE_PROFILE" >&3
}

minizitiConsole() {
    ingress_zone="$(getIngressZone)"
    console_url="https://miniziti-controller.${ingress_zone}/zac/"
    case "$DETECTED_OS" in
        "Windows")
            checkCommand wslview
            wslview "$console_url"
        ;;
        "macOS")
            checkCommand open
            open "$console_url"
        ;;
        *)
            checkCommand xdg-open
            xdg-open "$console_url"
        ;;
    esac
    logInfo "Opening ziti-console: $console_url ..."
}

listProfiles() {
    dir --format=single-column --ignore=default --sort=time --reverse "$PROFILES_DIR"
}

logger() {
    local caller="${FUNCNAME[1]}"

    if (( $# < 1 )); then
        echo "ERROR: $caller() takes 1 or more args" >&2
        return 1
    fi

    local message="$*"

    if [[ "$message" =~ ^r\'(.+)\'$ ]]; then
        raw_message="${BASH_REMATCH[1]}"
        message="$raw_message"
    fi

    caller_level="${caller##log}"
    if (( MINIZITI_DEBUG )); then
        line="${caller_level^^} ${FUNCNAME[2]}:${BASH_LINENO[1]}: $message"
    else
        line="${caller_level^^} $message"
    fi

    if [[ -n "${raw_message-}" ]]; then
        echo -E "$line"
    else
        echo -e "$line"
    fi
}

logInfo() {
    logger "$*"
}

logWarn() {
    logger "$*" >&2
}

logError() {
    logger "$*" >&2
}

logDebug() {
    logger "$*" >&3
}

controllerPod() {
    kubectlWrapper get pods \
        --selector app.kubernetes.io/component=ziti-controller \
        --output jsonpath='{.items[0].metadata.name}'
}

getPodStatus() {
    local pod_name="$1"
    kubectlWrapper get pod \
        --selector app.kubernetes.io/component="$pod_name" \
        --ignore-not-found=true \
        --output jsonpath='{.items[0].status.phase}' 2> /dev/null
}

showStatus() {
    COMPONENTS=("ziti-controller" "ziti-router")
    for component in "${COMPONENTS[@]}"; do
        status="$(getPodStatus "$component")"
        if [[ -z "$status" ]]; then
            logError "Could not retrieve status for component $component"
            exit 1
        fi
        echo "$component: $status"
    done
}

getDefaultProfile() {
    default_profile="$DEFAULT_PROFILE"
    if [[ -L "$default_profile" ]]; then
        basename "$(realpath "$default_profile")"
    else
        echo "miniziti"
    fi
}

setProfile() {
    profile="$1"
    profiles="$(listProfiles)"
    if ! echo "$profiles" | grep -q "$profile"; then
        logError "Invalid profile: $profile\nValid profiles are:\n$profiles"
        exit 1
    else
        ln -nsf "$PROFILES_DIR/$profile" "$DEFAULT_PROFILE"
        logInfo "Set default profile to: '$profile'"
    fi
}

zitiWrapper() {
    kubectlWrapper exec "$(controllerPod)" --container ziti-controller -- zitiLogin > /dev/null
    kubectlWrapper exec "$(controllerPod)" --container ziti-controller -- ziti "$@"
}

shellWrapper() {
    kubectlWrapper exec "$(controllerPod)" --container ziti-controller --tty --stdin -- bash
}

kubectlWrapper() {
    minikube kubectl --profile "$MINIKUBE_PROFILE" -- --context "$MINIKUBE_PROFILE" "$@"
}

minikubeWrapper() {
    minikube --profile "$MINIKUBE_PROFILE" "$@"
}

helmWrapper() {
    helm --kube-context "$MINIKUBE_PROFILE" "$@"
}

checkCommand() {
    if ! command -v "$1" &>/dev/null; then
        logError "this script requires command '$1'. Please install on the search PATH and try again."
        $1
    fi
}

main(){
    # xtrace opt implies --verbose
    if [[ $- =~ x ]]; then
        MINIZITI_DEBUG=1
        exec 3>&1
    else
        MINIZITI_DEBUG=0
    fi

    checkBashVersion >&2

    # require commands
    declare -a BINS=(awk grep helm jq minikube nslookup pgrep sed xargs)
    for BIN in "${BINS[@]}"; do
        checkCommand "$BIN"
    done

    # open a descriptor for debug messages
    exec 3>/dev/null

    # local strings with defaults that never produce an error
    declare DELETE_MINIZITI=0 \
            DETECTED_OS \
            DO_ZITI_LOGIN=0 \
            MINIKUBE_NODE_EXTERNAL \
            MINIKUBE_PROFILE \
            MINIZITI_HOSTS=1 \
            MINIZITI_MODIFY_HOSTS=0 \
            OPEN_ZITI_CONSOLE=0 \
            RUN_ZITI_CLI=0 \
            SAFETY_WAIT=1 \
            SHOW_ADMIN_CREDS=0 \
            START_MINIZITI=0 \
            ZITI_CHARTS_ALT=0 \
            ZITI_CHARTS_REF="openziti" \
            ZITI_CHARTS_URL="https://openziti.io/helm-charts/charts" \
            EXTRA_VALUES_DIR \
            ZITI_NAMESPACE

    # local arrays with defaults that never produce an error
    declare -a MINIKUBE_START_ARGS=()

    # local defaults that are inherited or may error
    DETECTED_OS="$(detectOs)"
    : "${DEBUG_MINIKUBE_TUNNEL:=0}"  # set env = 1 to trigger the minikube tunnel probe
    : "${MINIZITI_TIMEOUT_SECS:=240}"
    MINIZITI_CONFIGMAP="miniziti-config"
    ZITI_CLI_HOME="$(getZitiCliHome)"
    ZITI_CLI_CERTS_DIR="$ZITI_CLI_HOME/certs"
    STATE_DIR="$(makeMinizitiStateDir)"
    PROFILES_DIR="$STATE_DIR/profiles"
    [[ ! -d "$PROFILES_DIR" ]] && mkdir "$PROFILES_DIR"
    DEFAULT_PROFILE="$PROFILES_DIR/default"

    MINIKUBE_PROFILE="$(getDefaultProfile)"

    while (( $# )); do
        case "$1" in
            start)          START_MINIZITI=1
                            shift
            ;;
            delete)         DELETE_MINIZITI=1
                            shift
            ;;
            console)        OPEN_ZITI_CONSOLE=1
                            shift
            ;;
            creds)          SHOW_ADMIN_CREDS=1
                            shift
            ;;
            profile)
                            shift
                            if (( $# == 0 )); then
                                _usageProfile
                                exit 1
                            fi
                            case "$1" in
                                list)
                                    shift
                                    listProfiles
                                    exit
                                ;;
                                use)
                                    shift
                                    if (( $# != 1 )); then
                                        _usageProfileUse
                                        exit 1
                                    fi
                                    setProfile "$1"
                                    exit
                                ;;
                                show)
                                    getDefaultProfile
                                    exit
                                ;;
                                *)
                                    _usageProfile "$1"
                                    exit 1
                                ;;
                            esac
            ;;
            login)          DO_ZITI_LOGIN=1
                            shift
            ;;
            status)
                            showStatus
                            exit
            ;;
            ziti)           RUN_ZITI_CLI=1
                            shift
                            ziti_cli_args=("$@")
                            shift "${#ziti_cli_args[@]}"
            ;;
            kubectl)        shift
                            kubectlWrapper "${@:-}"
                            exit
            ;;
            minikube)       shift
                            minikubeWrapper "${@:-}"
                            exit
            ;;
            shell)          shift
                            shellWrapper "${@:-}"
                            exit
            ;;
            -p|--profile)   validateDnsName "$2"
                            MINIKUBE_PROFILE="$2"
                            # sanity check the profile name input
                            if [[ ${MINIKUBE_PROFILE} =~ ^- ]]; then
                                # in case the arg value is another option instead of the profile name
                                logError "--profile needs a profile name not starting with a hyphen"
                                _usage
                                exit 1
                            fi

                            shift 2
            ;;
            -n|--namespace) ZITI_NAMESPACE="$2"
                            shift 2
            ;;
            --charts)       ZITI_CHARTS_REF="${2%"/"}"
                            ZITI_CHARTS_URL="${2%"/"}"
                            ZITI_CHARTS_ALT=1
                            shift 2
            ;;
            --values-dir)   EXTRA_VALUES_DIR="${2%"/"}"
                            shift 2
            ;;
            -q|--quiet)     exec > /dev/null
                            shift
            ;;
            -v|--verbose|--debug)
                            MINIZITI_DEBUG=1
                            exec 3>&1
                            shift
            ;;
            --now)          SAFETY_WAIT=0
                            shift
            ;;
            --no-hosts)     MINIZITI_HOSTS=0
                            shift
            ;;
            --modify-hosts) if [[ "$DETECTED_OS" != "Linux" ]]; then
                                logError "The '--modify-hosts' option is only available for Linux"
                                exit 1
                            fi
                            MINIZITI_MODIFY_HOSTS=1
                            shift
            ;;
            --)             shift
                            mapfile -t -n1 MINIKUBE_START_ARGS <<< "$*"
                            shift $#
                            if (( ${#MINIKUBE_START_ARGS[*]} )) && [[ -n "${MINIKUBE_START_ARGS[0]}" ]]; then
                                logDebug "passing ${#MINIKUBE_START_ARGS[*]} args to minikube start: '${MINIKUBE_START_ARGS[*]}'"
                            else
                                MINIKUBE_START_ARGS=()
                            fi
            ;;
            -h|*help)       _usage
                            exit 0
            ;;
            *)              _usage "$1"
                            exit
            ;;
        esac
    done

    : "${ZITI_NAMESPACE:=${MINIKUBE_PROFILE}}"

    MINIZITI_INGRESS_ZONE="$MINIKUBE_PROFILE.internal"
    MINIZITI_INTERCEPT_ZONE="$MINIKUBE_PROFILE.private"
    PROFILE_DIR="$PROFILES_DIR/${MINIKUBE_PROFILE}"
    IDENTITIES_DIR="$PROFILE_DIR/identities"

    if (( DO_ZITI_LOGIN )); then
        minizitiLogin
        exit 0
    fi

    if (( SHOW_ADMIN_CREDS )); then
        showAdminCreds
        exit 0
    fi

    if (( OPEN_ZITI_CONSOLE )); then
        minizitiConsole
        exit 0
    fi

    if (( RUN_ZITI_CLI )); then
        zitiWrapper  "${ziti_cli_args[@]}"
        exit 0
    fi

    if (( DELETE_MINIZITI )); then
        banner "$MINIKUBE_PROFILE"

        trap EXIT
        if ingress_zone="$(getIngressZone)"; then
            if (( MINIZITI_MODIFY_HOSTS )) && grep -q "$ingress_zone" "$HOSTS_FILE"; then
                checkSudoRequired
                cleanHosts "$ingress_zone"
            fi

            CERT_FILE="$(find "$ZITI_CLI_CERTS_DIR" -maxdepth 1 -type f -name "miniziti-controller.$ingress_zone" -print -quit 2> /dev/null)"
            if [[ -n "$CERT_FILE" ]]; then
                logWarn "Deleting miniziti certificate file: $CERT_FILE"
                rm -f  "$CERT_FILE"
            fi
        fi
        trap - EXIT

        deleteMiniziti 10

        if [[ -d "$PROFILE_DIR" ]]; then
            logWarn "Deleting miniziti profile directory: $PROFILE_DIR"
            rm -rf  "$PROFILE_DIR"
        fi

        if [[ -L "$DEFAULT_PROFILE" ]]; then
            if [[ "$(basename "$(realpath "$DEFAULT_PROFILE")")" == "$MINIKUBE_PROFILE" ]]; then
                unlink "$DEFAULT_PROFILE"
            fi
        fi

        # Cannot nicely call logout until https://github.com/openziti/ziti/issues/1305 is addressed.
        # if checkCommand ziti &>/dev/null; then
        #     logWarn "Removing $MINIKUBE_PROFILE profile identity from ziti-cli.json"
        #     ziti edge logout --cli-identity "$MINIKUBE_PROFILE" >&3
        # fi

        exit 0
    fi

    if (( START_MINIZITI != 1 )); then
        _usage
        exit 0;
    fi


    banner "$MINIKUBE_PROFILE"

    if [[ ! -d "$IDENTITIES_DIR" ]]; then
        logDebug "Creating miniziti identities directory: ($IDENTITIES_DIR)"
        mkdir -p "$IDENTITIES_DIR"
    fi

    #
    ## Ensure Minikube is Started and Configured
    #

    # run 'minikube start' if not running or any extra start args are present
    logInfo "waiting for minikube to be ready"
    if  ! minikube status \
            --profile "${MINIKUBE_PROFILE}" 2>/dev/null \
        | grep -q "apiserver: Running" \
        || (( ${#MINIKUBE_START_ARGS[*]} )); then
        logDebug "apiserver not running or got extra start args, running 'minikube start'"
        eval minikube start \
            --profile "${MINIKUBE_PROFILE}" \
            "${MINIKUBE_START_ARGS[*]}" >&3
    else
        logDebug "apiserver is running, not starting minikube"
    fi

    MINIKUBE_NODE_EXTERNAL=$(minikube --profile "${MINIKUBE_PROFILE}" ip)
    # if --no-hosts then build a new zone name for RFC-1918 wildcard DNS
    (( MINIZITI_HOSTS )) || {
        MINIZITI_INGRESS_ZONE="${MINIKUBE_NODE_EXTERNAL}.sslip.io"
        logDebug "DNS wildcard zone for ingresses is ${MINIZITI_INGRESS_ZONE}"
    }

    if [[ -n "${MINIKUBE_NODE_EXTERNAL:-}" ]]; then
        logDebug "the minikube external IP is ${MINIKUBE_NODE_EXTERNAL}"
    else
        logError "failed to find minikube external IP"
        exit 1
    fi

    if (( MINIZITI_MODIFY_HOSTS )); then
        installHosts
    fi

    # verify current context can connect to apiserver
    kubectlWrapper cluster-info >&3
    logDebug "kubectl successfully obtained cluster-info from apiserver"

    # enable ssl-passthrough for OpenZiti ingresses
    if kubectlWrapper get deployment "ingress-nginx-controller" \
        --namespace ingress-nginx \
        --output 'go-template={{ (index .spec.template.spec.containers 0).args }}' 2>/dev/null \
        | grep -q enable-ssl-passthrough; then
        logDebug "ingress-nginx has ssl-passthrough enabled"
    else
        logDebug "installing ingress-nginx"
        # enable minikube addons for ingress-nginx
        minikube addons enable ingress \
            --profile "${MINIKUBE_PROFILE}" >&3
        # enable minikube addon ingress-dns unless --no-hosts
        (( MINIZITI_HOSTS )) && {
            minikube addons enable ingress-dns \
                --profile "${MINIKUBE_PROFILE}" >&3
        }
        logDebug "patching ingress-nginx deployment to enable ssl-passthrough"
        kubectlWrapper patch deployment "ingress-nginx-controller" \
            --namespace ingress-nginx \
            --type json \
            --patch '[{"op": "add",
                "path": "/spec/template/spec/containers/0/args/-",
                "value":"--enable-ssl-passthrough"
            }]' >&3
    fi

    logInfo "waiting for ingress-nginx to be ready"
    # wait for ingress-nginx
    kubectlWrapper wait jobs "ingress-nginx-admission-patch" \
        --namespace ingress-nginx \
        --for condition=complete \
        --timeout "${MINIZITI_TIMEOUT_SECS}s" >&3

    kubectlWrapper wait pods \
        --namespace ingress-nginx \
        --for condition=ready \
        --selector app.kubernetes.io/component=controller \
        --timeout "${MINIZITI_TIMEOUT_SECS}s" >&3

    declare -A HELM_REPOS
    HELM_REPOS[openziti]="openziti.io/helm-charts"
    HELM_REPOS[jetstack]="charts.jetstack.io"
    HELM_REPOS[ingress-nginx]="kubernetes.github.io/ingress-nginx"
    for REPO in "${!HELM_REPOS[@]}"; do
        if helmWrapper repo list | cut -f1 | grep -qE "^${REPO}(\s+)?$"; then
            logDebug "refreshing ${REPO} Helm Charts"
            helmWrapper repo update "${REPO}" >&3
        else
            logInfo "subscribing to ${REPO} Helm Charts"
            helmWrapper repo add "${REPO}" "https://${HELM_REPOS[${REPO}]}" >&3
        fi
    done

    helmWrapper upgrade --install cert-manager jetstack/cert-manager \
        --namespace cert-manager --create-namespace \
        --set crds.enabled=true >&3
    kubectlWrapper wait deployments -n cert-manager --for condition=Available --timeout="${MINIZITI_TIMEOUT_SECS}s" --all >&3

    kubectlWrapper get namespace "${ZITI_NAMESPACE}" &>/dev/null || kubectlWrapper create namespace "${ZITI_NAMESPACE}" >&3
    helmWrapper upgrade --install trust-manager jetstack/trust-manager \
        --namespace cert-manager \
        --set crds.keep=false \
        --set app.trust.namespace="${ZITI_NAMESPACE}" >&3
    kubectlWrapper wait deployments -n cert-manager --for condition=Available --timeout="${MINIZITI_TIMEOUT_SECS}s" trust-manager >&3

    #
    ## Ensure OpenZiti Controller is Upgraded and Ready
    #

    logInfo "installing openziti controller chart"
    if (( ZITI_CHARTS_ALT )) && [[ -s "${ZITI_CHARTS_REF}/ziti-controller/Chart.lock" ]]; then
        helmWrapper dependency build "${ZITI_CHARTS_REF}/ziti-controller" >&3
    fi
    local -a _controller_cmd=(upgrade --install "ziti-controller" "${ZITI_CHARTS_REF}/ziti-controller"
        --namespace "${ZITI_NAMESPACE}" --create-namespace
        --set clientApi.advertisedHost="miniziti-controller.${MINIZITI_INGRESS_ZONE}"
        --values "${ZITI_CHARTS_URL}/ziti-controller/values-ingress-nginx.yaml"
        --set ctrlPlane.service.enabled=false
        --set ctrlPlane.ingress.enabled=false
    )
    if [[ -n "${EXTRA_VALUES_DIR:-}" && -s "${EXTRA_VALUES_DIR}/ziti-controller.yaml" ]]; then
        _controller_cmd+=(--values "${EXTRA_VALUES_DIR}/ziti-controller.yaml")
    fi
    helmWrapper "${_controller_cmd[@]}" >&3

    logDebug "setting default namespace '${ZITI_NAMESPACE}' in kubeconfig context '${MINIKUBE_PROFILE}'"
        kubectlWrapper config set-context "${MINIKUBE_PROFILE}" \
            --namespace "${ZITI_NAMESPACE}" >&3

    logInfo "waiting for ziti-controller to be ready"
    kubectlWrapper wait deployments ziti-controller \
            --namespace "${ZITI_NAMESPACE}" \
            --for condition=Available=True \
            --timeout "${MINIZITI_TIMEOUT_SECS}s" >&3

    #
    ## Ensure Minikube Tunnel is Running on macOS and WSL
    #

    # wait to probe for the minikube tunnel until after controller deployment so there's at least one
    # ingress causing minikube to immediately prompt for sudo password
    if  (( DEBUG_MINIKUBE_TUNNEL )) || [[ "${DETECTED_OS}" =~ Windows|macOS ]]; then
        logDebug "detected OS is ${DETECTED_OS}, probing for minikube tunnel"
        if ! pgrep -f "minikube --profile ${MINIKUBE_PROFILE} tunnel" >&3; then
            echo -e "ERROR: ${DETECTED_OS} OS requires a running minikube tunnel for ingresses."\
                    " In another terminal, run the following command. Then re-run this script."\
                    "\n\n\tminikube --profile ${MINIKUBE_PROFILE} tunnel\n" >&2
            exit 1
        else
            logDebug "minikube tunnel is running"
        fi
        # recommend /etc/hosts change unless dns is configured to reach the minikube node IP
        checkDns "127.0.0.1"
    else
        checkDns "$MINIKUBE_NODE_EXTERNAL"
    fi

    #
    ## Ensure Cluster DNS is Resolving miniziti-controller.${MINIZITI_INGRESS_ZONE}
    #

    if ! testClusterDns "${MINIKUBE_NODE_EXTERNAL}" 2>/dev/null; then
        logDebug "initial cluster dns test failed, doing cluster dns setup"

        # xargs trims whitespace because minikube ssh returns a stray trailing '\r' after remote command output
        logDebug "probing minikube node for internal host record"
        MINIKUBE_NODE_INTERNAL=$(minikube --profile "${MINIKUBE_PROFILE}" ssh 'grep host.minikube.internal /etc/hosts')

        if [[ -n "${MINIKUBE_NODE_INTERNAL:-}" ]]; then
            # strip surrounding whitespace
            MINIKUBE_NODE_INTERNAL=$(xargs <<< "${MINIKUBE_NODE_INTERNAL}")
            logDebug "the minikube internal host record is \"${MINIKUBE_NODE_INTERNAL}\""
        else
            logError "failed to find minikube internal IP"
            exit 1
        fi

        (( MINIZITI_HOSTS )) && {
            logDebug "patching coredns configmap with *.${MINIZITI_INGRESS_ZONE} forwarder to minikube ingress-dns nameserver"
            kubectlWrapper patch configmap "coredns" \
                --namespace kube-system \
                --patch "
        data:
            Corefile: |
                .:53 {
                    log
                    errors
                    health {
                        lameduck 5s
                    }
                    ready
                    kubernetes cluster.local in-addr.arpa ip6.arpa {
                        pods insecure
                        fallthrough in-addr.arpa ip6.arpa
                        ttl 30
                    }
                    prometheus :9153
                    hosts {
                        ${MINIKUBE_NODE_INTERNAL}
                        fallthrough
                    }
                    forward . /etc/resolv.conf {
                        max_concurrent 1000
                    }
                    cache 30
                    loop
                    reload
                    loadbalance
                }
                ${MINIZITI_INGRESS_ZONE}:53 {
                    errors
                    cache 30
                    forward . ${MINIKUBE_NODE_EXTERNAL}
                }
        " >&3

            logDebug "deleting coredns pod so a new one will have modified Corefile"
            kubectlWrapper delete pods \
                --context "$MINIKUBE_PROFILE" \
                --namespace kube-system \
                --selector k8s-app=kube-dns >&3

            logDebug "waiting for cluster dns to be ready"
            kubectlWrapper wait deployments "coredns" \
                --namespace kube-system \
                --for condition=Available=True \
                --timeout "${MINIZITI_TIMEOUT_SECS}s" >&3
        }

        # perform a DNS query in a pod so we know ingress-dns is working inside the cluster
        testClusterDns "${MINIKUBE_NODE_EXTERNAL}"
    fi

    if kubectlWrapper get configmap "$MINIZITI_CONFIGMAP" &> /dev/null; then
        logDebug "$MINIZITI_CONFIGMAP configmap has been applied"
    else
        logInfo "Applying $MINIZITI_CONFIGMAP configmap"
        cat <<EOF | kubectlWrapper apply -f - >&3
apiVersion: v1
kind: ConfigMap
metadata:
  name: "$MINIZITI_CONFIGMAP"
  namespace: "$ZITI_NAMESPACE"
data:
  ingress-zone: "$MINIZITI_INGRESS_ZONE"
EOF
    fi

    #
    ## Ensure OpenZiti Router is Enrolled and Ready
    #
    #

    logInfo "Setting default ziti identity to: $MINIKUBE_PROFILE"
    zitiWrapper edge use "$MINIKUBE_PROFILE" >&3

    ROUTER_NAME='miniziti-router'
    ROUTER_OTT="$IDENTITIES_DIR/$ROUTER_NAME.jwt"
    if  zitiWrapper edge list edge-routers "name=\"$ROUTER_NAME\"" \
        | grep -q miniziti-router; then
        logDebug "updating $ROUTER_NAME"
        zitiWrapper edge update edge-router "$ROUTER_NAME" \
            --role-attributes "public-routers" >&3
    else
        logDebug "creating $ROUTER_NAME"
        zitiWrapper edge create edge-router "$ROUTER_NAME" \
            --role-attributes "public-routers" \
            --tunneler-enabled >&3
        zitiWrapper edge list edge-routers \
            "name=\"$ROUTER_NAME\"" \
            --output-json \
            | jq --exit-status --raw-output '.data[0].enrollmentJwt' > "$ROUTER_OTT"
    fi

    logDebug "installing router chart as 'ziti-router'"
    if (( ZITI_CHARTS_ALT )) && [[ -s "${ZITI_CHARTS_REF}/ziti-router/Chart.lock" ]]; then
        helmWrapper dependency build "${ZITI_CHARTS_REF}/ziti-router" >&3
    fi
    local -a _router_cmd=(upgrade --install "ziti-router" "${ZITI_CHARTS_REF}/ziti-router"
        --namespace "${ZITI_NAMESPACE}"
        --set-file enrollmentJwt="$ROUTER_OTT"
        --set edge.advertisedHost="miniziti-router.${MINIZITI_INGRESS_ZONE}"
        --set ctrl.endpoint="miniziti-controller.${MINIZITI_INGRESS_ZONE}:443"
        --set "tunnel.mode=host"
        --values "${ZITI_CHARTS_URL}/ziti-router/values-ingress-nginx.yaml"
        --set linkListeners.transport.service.enabled=false
        --set linkListeners.transport.ingress.enabled=false
    )
    if [[ -n "${EXTRA_VALUES_DIR:-}" && -s "${EXTRA_VALUES_DIR}/ziti-router.yaml" ]]; then
        _router_cmd+=(--values "${EXTRA_VALUES_DIR}/ziti-router.yaml")
    fi
    helmWrapper "${_router_cmd[@]}" >&3

    logInfo "waiting for ziti-router to be ready"
    kubectlWrapper wait deployments "ziti-router" \
        --namespace "${ZITI_NAMESPACE}" \
        --for condition=Available=True \
        --timeout "${MINIZITI_TIMEOUT_SECS}s" >&3

    logDebug "probing miniziti-router for online status"
    if zitiWrapper edge list edge-routers "name=\"$ROUTER_NAME\"" \
        | awk '/miniziti-router/ {print $6}' \
        | grep -q true; then
        logInfo "miniziti-router is online"
    else
        logError "miniziti-router is offline"
        exit 1
    fi

    logDebug "setting default namespace to '${ZITI_NAMESPACE}' in kubeconfig context '${MINIKUBE_PROFILE}'"
        kubectlWrapper config set-context "${MINIKUBE_PROFILE}" \
            --namespace "${ZITI_NAMESPACE}" >&3

    #
    ## Ensure OpenZiti Identities and Services are Created
    #

    CLIENT_NAME="${MINIKUBE_PROFILE}-client"
    CLIENT_OTT="$IDENTITIES_DIR/$CLIENT_NAME.jwt"

    if  ! zitiWrapper edge list identities "name=\"$CLIENT_NAME\"" --csv \
        | grep -q "$CLIENT_NAME"; then
        logDebug "creating identity $CLIENT_NAME"
        zitiWrapper edge create identity "$CLIENT_NAME" \
            --role-attributes httpbin-clients >&3
        zitiWrapper edge list identities "name=\"$CLIENT_NAME\"" \
            --output-json \
        | jq --exit-status --raw-output '.data[0].enrollment.ott.jwt' > "$CLIENT_OTT"
    else
        logDebug "ignoring identity $CLIENT_NAME"
    fi

    HTTPBIN_NAME="httpbin-host"
    HTTPBIN_OTT="$IDENTITIES_DIR/$HTTPBIN_NAME.jwt"

    if  ! zitiWrapper edge list identities "name=\"$HTTPBIN_NAME\"" --csv \
        | grep -q "$HTTPBIN_NAME"; then
        logDebug "creating identity $HTTPBIN_NAME"
        zitiWrapper edge create identity "$HTTPBIN_NAME" \
            --role-attributes httpbin-hosts >&3
        zitiWrapper edge list identities "name=\"$HTTPBIN_NAME\"" \
            --output-json \
        | jq --exit-status --raw-output '.data[0].enrollment.ott.jwt' > "$HTTPBIN_OTT"

    else
        logDebug "ignoring identity $HTTPBIN_NAME"
    fi

    if  ! zitiWrapper edge list configs 'name="httpbin-intercept-config"' --csv \
        | grep -q "httpbin-intercept-config"; then
        logDebug "creating config httpbin-intercept-config"
        zitiWrapper edge create config "httpbin-intercept-config" intercept.v1 \
            '{"protocols":["tcp"],"addresses":["httpbin.'"${MINIZITI_INTERCEPT_ZONE}"'"], "portRanges":[{"low":80, "high":80}]}' >&3
    else
        logDebug "ignoring config httpbin-intercept-config"
    fi

    if  ! zitiWrapper edge list configs 'name="httpbin-host-config"' --csv \
        | grep -q "httpbin-host-config"; then
        logDebug "creating config httpbin-host-config"
        zitiWrapper edge create config "httpbin-host-config" host.v1 \
            '{"protocol":"tcp", "address":"httpbin","port":8080}' >&3
    else
        logDebug "ignoring config httpbin-host-config"
    fi

    if  ! zitiWrapper edge list services 'name="httpbin-service"' --csv \
        | grep -q "httpbin-service"; then
        logDebug "creating service httpbin-service"
        zitiWrapper edge create service "httpbin-service" \
            --configs httpbin-intercept-config,httpbin-host-config >&3
    else
        logDebug "ignoring service httpbin-service"
    fi

    if  ! zitiWrapper edge list service-policies 'name="httpbin-bind-policy"' --csv \
        | grep -q "httpbin-bind-policy"; then
        logDebug "creating service-policy httpbin-bind-policy"
        zitiWrapper edge create service-policy "httpbin-bind-policy" Bind --semantic "AnyOf" \
            --service-roles '@httpbin-service' --identity-roles '#httpbin-hosts' >&3
    else
        logDebug "ignoring service-policy httpbin-bind-policy"
    fi

    if  ! zitiWrapper edge list service-policies 'name="httpbin-dial-policy"' --csv \
        | grep -q "httpbin-dial-policy"; then
        logDebug "creating service-policy httpbin-dial-policy"
        zitiWrapper edge create service-policy "httpbin-dial-policy" Dial --semantic "AnyOf" \
            --service-roles '@httpbin-service' --identity-roles '#httpbin-clients' >&3
    else
        logDebug "ignoring service-policy httpbin-dial-policy"
    fi

    if  ! zitiWrapper edge list edge-router-policies 'name="public-routers"' --csv \
        | grep -q "public-routers"; then
        logDebug "creating edge-router-policy public-routers"
        zitiWrapper edge create edge-router-policy "public-routers" \
            --edge-router-roles '#public-routers' --identity-roles '#all' >&3
    else
        logDebug "ignoring edge-router-policy public-routers"
    fi

    if  ! zitiWrapper edge list service-edge-router-policies 'name="public-routers"' --csv \
        | grep -q "public-routers"; then
        logDebug "creating service-edge-router-policy public-routers"
        zitiWrapper edge create service-edge-router-policy "public-routers" \
            --edge-router-roles '#all' --service-roles '#all' >&3
    else
        logDebug "ignoring service-edge-router-policy public-routers"
    fi

    if [[ -s "$HTTPBIN_OTT" ]]; then
        logDebug "installing httpbin chart as 'miniziti-httpbin'"
        helmWrapper install "miniziti-httpbin" "${ZITI_CHARTS_REF}/httpbin" \
            --set-file zitiEnrollment="$HTTPBIN_OTT" \
            --set zitiServiceName=httpbin-service >&3
        rm -f "$HTTPBIN_OTT"
        logDebug "deleted $HTTPBIN_OTT after installing successfully with miniziti-httpbin chart"
    else
        logDebug "upgrading httpbin chart as 'miniziti-httpbin'"
        helmWrapper upgrade "miniziti-httpbin" "${ZITI_CHARTS_REF}/httpbin" \
            --set-file zitiEnrollment=<(echo "enrolled") \
            --set zitiServiceName=httpbin-service >&3
    fi

    echo -e "\n\n"
    logInfo "Your OpenZiti Console is here: https://miniziti-controller.${MINIZITI_INGRESS_ZONE}/zac/"
    showAdminCreds
    echo -e "\n\n"

    logInfo "r'Success! Remember to add your edge client identity '$(pathToNative "$CLIENT_OTT")' in your tunneler, e.g. Ziti Desktop Edge.'"
}

main "$@"
