0

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)

Brad Parks
  • 66,836
  • 64
  • 257
  • 336
  • 1
    something similar is answered 3 years back and not yet contradicted authoritatively. You may find the answer here https://stackoverflow.com/a/51365714/6309601 – P.... Jan 12 '22 at 21:06
  • 1
    thanks - I will check it out! – Brad Parks Jan 12 '22 at 21:41
  • Can I help you with other information? If you think that my answer helped you, please consider accepting it by clicking the check mark (✔️) on the left side under the vote arrows. Should change the color to green. I'd really appreciate it. Thanks! – Nestor Daniel Ortega Perez Jan 18 '22 at 23:57
  • 1
    @NestorDanielOrtegaPerez - thanks for the help! something higher priority came up at work, and I will get back to this within the week to evaluate the answers – Brad Parks Jan 19 '22 at 13:07

3 Answers3

2

When you use the $ kubectl port-forward service/my-service :12345 command, you must get an output like this:

Forwarding from 127.0.0.1:XXXX -> YYYY
Forwarding from [::1]:XXXX -> YYYY

And that’s the only way you have to identify the local chosen port, you can add to your script an instruction to read that value form the output. The XXXX number is the number of the local chosen port. I can suggest you to use service type Nodeport instead of port-forward. Using NodePort, you are able to expose your app as a service and have access from outside the Kubernetes. Take a look into the following documentation for more examples.

1

This(bash function) may not be a cleaner way but works on a kubeadm cluster. Tried with multiple forwarding same time for different pods/svc.

get_local_port()
{
port="${1}"
if [ -z "${port}" ];then
    echo "[Error]: 1st argument should be the remote port number"
    return 1
fi
netstat -plnt |\
awk -vpid="$(ps -eaf |awk -v port=${port} '{if(match($0,"port-forward.*"port)) printf "%s|", $2}'|sed -r 's/.$//g')" '{if(match($NF,"^"pid"/")) {split($4,a,":");print a[2]}}'

}

Usage:

get_local_port <remote-port-number>

Explanation:

This command is parsing the ps -eaf to get the PID of the port-forwarding command and later parses it over netstat to get the local port.

P....
  • 17,421
  • 2
  • 32
  • 52
0

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())
Brad Parks
  • 66,836
  • 64
  • 257
  • 336