@ -14,6 +14,7 @@
import functools
import json
import logging
import re
import subprocess
import time
from typing import List , Optional , Tuple
@ -78,9 +79,96 @@ class PortForwardingError(Exception):
""" Error forwarding port """
class PortForwarder :
PORT_FORWARD_LOCAL_ADDRESS : str = ' 127.0.0.1 '
def __init__ ( self ,
context : str ,
namespace : str ,
destination : str ,
remote_port : int ,
local_port : Optional [ int ] = None ,
local_address : Optional [ str ] = None ) :
self . context = context
self . namespace = namespace
self . destination = destination
self . remote_port = remote_port
self . local_address = local_address or self . PORT_FORWARD_LOCAL_ADDRESS
self . local_port : Optional [ int ] = local_port
self . subprocess : Optional [ subprocess . Popen ] = None
def connect ( self ) - > None :
port_mapping = f " { self . local_port } : { self . remote_port } " if self . local_port else f " : { self . remote_port } "
cmd = [
" kubectl " , " --context " , self . context , " --namespace " , self . namespace ,
" port-forward " , " --address " , self . local_address , self . destination ,
port_mapping
]
self . subprocess = subprocess . Popen ( cmd ,
stdout = subprocess . PIPE ,
stderr = subprocess . STDOUT ,
universal_newlines = True )
# Wait for stdout line indicating successful start.
if self . local_port :
local_port_expected = (
f " Forwarding from { self . local_address } : { self . local_port } "
f " -> { self . remote_port } " )
else :
local_port_re = re . compile (
f " Forwarding from { self . local_address } :([0-9]+) -> { self . remote_port } "
)
try :
while True :
time . sleep ( 0.05 )
output = self . subprocess . stdout . readline ( ) . strip ( )
if not output :
return_code = self . subprocess . poll ( )
if return_code is not None :
errors = [
error
for error in self . subprocess . stdout . readlines ( )
]
raise PortForwardingError (
' Error forwarding port, kubectl return '
f ' code { return_code } , output { errors } ' )
# If there is no output, and the subprocess is not exiting,
# continue waiting for the log line.
continue
# Validate output log
if self . local_port :
if output != local_port_expected :
raise PortForwardingError (
f ' Error forwarding port, unexpected output { output } '
)
else :
groups = local_port_re . search ( output )
if groups is None :
raise PortForwardingError (
f ' Error forwarding port, unexpected output { output } '
)
# Update local port to the randomly picked one
self . local_port = int ( groups [ 1 ] )
logger . info ( output )
break
except Exception :
self . close ( )
raise
def close ( self ) - > None :
if self . subprocess is not None :
logger . info ( ' Shutting down port forwarding, pid %s ' ,
self . subprocess . pid )
self . subprocess . kill ( )
stdout , _ = self . subprocess . communicate ( timeout = 5 )
logger . info ( ' Port forwarding stopped ' )
logger . debug ( ' Port forwarding remaining stdout: %s ' , stdout )
self . subprocess = None
class KubernetesNamespace :
NEG_STATUS_META = ' cloud.google.com/neg-status '
PORT_FORWARD_LOCAL_ADDRESS : str = ' 127.0.0.1 '
DELETE_GRACE_PERIOD_SEC : int = 5
WAIT_SHORT_TIMEOUT_SEC : int = 60
WAIT_SHORT_SLEEP_SEC : int = 1
@ -302,54 +390,13 @@ class KubernetesNamespace:
remote_port : int ,
local_port : Optional [ int ] = None ,
local_address : Optional [ str ] = None ,
) - > subprocess . Popen :
""" Experimental """
local_address = local_address or self . PORT_FORWARD_LOCAL_ADDRESS
local_port = local_port or remote_port
cmd = [
" kubectl " , " --context " , self . api . context , " --namespace " , self . name ,
" port-forward " , " --address " , local_address ,
f " pod/ { pod . metadata . name } " , f " { local_port } : { remote_port } "
]
pf = subprocess . Popen ( cmd ,
stdout = subprocess . PIPE ,
stderr = subprocess . STDOUT ,
universal_newlines = True )
# Wait for stdout line indicating successful start.
expected = ( f " Forwarding from { local_address } : { local_port } "
f " -> { remote_port } " )
try :
while True :
time . sleep ( 0.05 )
output = pf . stdout . readline ( ) . strip ( )
if not output :
return_code = pf . poll ( )
if return_code is not None :
errors = [ error for error in pf . stdout . readlines ( ) ]
raise PortForwardingError (
' Error forwarding port, kubectl return '
f ' code { return_code } , output { errors } ' )
elif output != expected :
raise PortForwardingError (
f ' Error forwarding port, unexpected output { output } ' )
else :
logger . info ( output )
break
except Exception :
self . port_forward_stop ( pf )
raise
# TODO(sergiitk): return new PortForwarder object
) - > PortForwarder :
pf = PortForwarder ( self . api . context , self . name ,
f " pod/ { pod . metadata . name } " , remote_port , local_port ,
local_address )
pf . connect ( )
return pf
@staticmethod
def port_forward_stop ( pf ) :
logger . info ( ' Shutting down port forwarding, pid %s ' , pf . pid )
pf . kill ( )
stdout , _stderr = pf . communicate ( timeout = 5 )
logger . info ( ' Port forwarding stopped ' )
logger . debug ( ' Port forwarding remaining stdout: %s ' , stdout )
@staticmethod
def _pod_started ( pod : V1Pod ) :
return pod . status . phase not in ( ' Pending ' , ' Unknown ' )