Search code examples
kubectlportforwarding

kubectl - determine local port used when port-forwarding on random local port?


kubectl lets one port forward to a remote service like so

$ kubectl port-forward service/my-service :12345 

How can I find out the local port that was chosen, and used by kubectl in this command?

Additional details: I'd like to be able to determine this from a script, that looks for an active port forward for a service. If a port forward isn't found, it creates a port forward on a free local port, and returns the port number (which is what the above does)


Solution

  • I couldn't get any other approach to work, so I ended writing a script that dynamically finds a free port, and sets up a forward, then returns the port. If a matching port forward already exists, it can return that instead. Sample usage below. Note that currently I use another script to determine a free local port, and then use that port number to setup the port forward, and could've instead parsed the output from the generic port-forward command I mentioned in the question, but this approach is less fragile, and more re-usable, I believe.

    $ kpf help
    
      kubernetes port forward helper
      * helps you transparently ensure a port forward is setup for a service.
    
      usage: kpf [require|get|create|remove|remove_all] {SERVICEI} {REMOTE_PORT} {EXTRA_ARGS}
    
      e.g.
    
      # get or create a port forward for your-service on remote port 8080
      # returns the port if successful, or fails and shows an error message
      kpf require service/your-service 8080
    
      # return any existing port forwards for your-service on remote port 8080, or nothing
      kpf get service/your-service 8080
    
      # create a new local port forward for your-service on remote port 8080, returning port used
      kpf create service/your-service 8080
    
      # remove any port forwards found to your-service on remote port 8080
      kpf remove service/your-service 8080
    
      # remove ALL port forwards to any kubernetes remote ports
      kpf remove_all
    

    kpf

    #!/usr/bin/env bash
    DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; PATH="$DIR:$PATH"
    ME=$(basename "$0")
    
    function show_help()
    {
      IT=$(cat <<EOF
    
      kubernetes port forward helper
      * helps you transparently ensure a port forward is setup for a service.
    
      usage: $ME [require|get|create|remove|remove_all] {SERVICE} {REMOTE_PORT} {EXTRA_ARGS}
    
      e.g.
    
      # get or create a port forward for your-service on remote port 8080
      # returns the port if successful, or fails and shows an error message
      $ME require service/your-service 8080
    
      # return any existing port forwards for your-service on remote port 8080, or nothing
      $ME get service/your-service 8080
    
      # create a new local port forward for your-service on remote port 8080, returning port used
      $ME create service/your-service 8080
    
      # remove any port forwards found to your-service on remote port 8080
      $ME remove service/your-service 8080
    
      # remove ALL port forwards to any kubernetes remote ports
      $ME remove_all 
    
      # create a port forward on local port 4569, for localstack, in namespace localstack
      $ME create svc/localstack 4569:4569 --namespace localstack  
    EOF
    )
      echo "$IT"
      echo
      exit
    }
    
    if [ -z "$1" ]
    then
      show_help
    fi
    if [ "$1" = "help" ] || [ "$1" = '?' ] || [ "$1" = "--help" ] || [ "$1" = "h" ]; then
      show_help
    fi
    
    OP=$1
    SERVICE=$2
    REMOTE_PORT=$3
    if [ -n "$4" ]
    then
      shift;
      shift;
      shift;
      EXTRA_ARGS="$@"
    fi
    
    LOG_FILE_DIR="/tmp/__${ME}"
    
    function getExistingPortForward(){
      listPortForwards | grep "$SERVICE" | grep -e ":$REMOTE_PORT" | head -n1
    }
    
    function coln(){
      COL=$1
      DELIM=${2:-' '}
      awk -F"$DELIM" -v col="$COL" '{print $col}' | sort | uniq
    }
    
    function err(){
      echo "$@" >&2; 
    }
    
    function isNumeric(){
      INPUT=$*
      case ${INPUT#[-+]} in
        *[!0-9]* ) echo NO ;;
        * ) echo YES ;;
      esac
    }
    
    function getPid(){
      local INFO=$(getExistingPortForward | awk '{print $2}')
    
      if [ "$(isNumeric "$INFO")" == "YES" ]; then
        echo "$INFO"
      fi
    }
    
    function getLocalPort(){
      local INFO=$(getExistingPortForward)
      INFO=$(echo "$INFO" | awk -F ":$REMOTE_PORT" '{print $1}' | awk '{print $NF}')
    
      if [ "$(isNumeric "$INFO")" == "YES" ]; then
        echo "$INFO"
      fi
    }
    
    function removePortForward(){
      local PID=$(getPid)
      if [ -z "$PID" ]
      then
        return;
      fi
    
      set -x
      kill "$PID"
    }
    
    function portForward(){
      local LOCAL_PORT=$1
      local FILE=$2
    
      kubectl port-forward "$SERVICE" "$LOCAL_PORT":"$REMOTE_PORT" $EXTRA_ARGS > $FILE 2>&1 &
    }
    
    function failIfFileContains(){
      local FILE=$1
      local SUBSTR=$2
      local MSG=$3
      if grep -iq "$SUBSTR" "$FILE"; then
        cat "$FILE" >&2
        err "Failed: $MSG"
        err "Service:$SERVICE, $REMOTE_PORT $EXTRA_ARGS"
        err
        exit 1
      fi
    }
    
    function getTargetLocalPort(){
      local RESULT
      if [[ $REMOTE_PORT == *":"* ]]; then
        RESULT=$(echo $REMOTE_PORT | coln 1 ':')
        REMOTE_PORT=$(echo $REMOTE_PORT | coln 2 ':')
      else
        RESULT=$(port_find_free_local_port)
      fi
      echo "$RESULT"
    }
    
    function createPortForward(){
      local LOCAL_PORT=$(getTargetLocalPort)
      local LOG_FILE="${LOG_FILE_DIR}/${LOCAL_PORT}_log.txt"
      mkdir -p "$LOG_FILE_DIR"
      rm -f "$LOG_FILE"
    
      portForward "$LOCAL_PORT" "$LOG_FILE"
    
      # wait for the log file to indicate success or failure
      while true
      do
        sleep 0.5
        failIfFileContains "$LOG_FILE" "address already" "Port $LOCAL_PORT already in use"
        failIfFileContains "$LOG_FILE" "must be logged" "Please set some PCSK variables, and try again"
        failIfFileContains "$LOG_FILE" "Failed" "Please review the above log, and try again"
        failIfFileContains "$LOG_FILE" "Error" "Please review the above log, and try again"
        if grep -q Forwarding "$LOG_FILE"; then
          # port forward successful
          echo "$LOCAL_PORT"
          break
        fi
      done
    }
    
    function getOrCreatePortForward(){
      local PORT=$(getLocalPort)
      if [ -n "$PORT" ]
      then
        echo "$PORT"
        exit;
      fi
    
      createPortForward
    }
    
    function listPortForwards(){
      ps aux | grep kubectl | grep port-forward | sort
    }
    
    function removeAll(){
      listPortForwards | awk '{print $2}' | xargs kill -9
      rm -fr "$LOG_FILE_DIR"
    }
    
    function require3Args(){
      if [ -z "$REMOTE_PORT" ]
      then
        show_help
      fi
    }
    
    if [ "$OP" = "get" ]; then
      require3Args
      getLocalPort
    elif [ "$OP" = "create" ]; then
      require3Args
      createPortForward
    elif [ "$OP" = "require" ]; then
      require3Args
      getOrCreatePortForward
    elif [ "$OP" = "remove" ]; then
      require3Args
      removePortForward
    elif [ "$OP" = "list" ]; then
      listPortForwards
    elif [ "$OP" = "remove_all" ]; then
      removeAll
    else
      show_help
    fi
    

    port_find_free_local_port

    #!/usr/bin/env python
    # https://stackoverflow.com/a/45690594/26510
    
    import socket
    from contextlib import closing
    
    def find_free_port():
        with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
            s.bind(('', 0))
            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            return s.getsockname()[1]
    
    print(find_free_port())