fix(kubernetes): temporary solution for updated k8s python client
This commit is contained in:
parent
07d6fe7442
commit
9129813244
1478 changed files with 422354 additions and 2 deletions
15
kubernetes/base/stream/__init__.py
Normal file
15
kubernetes/base/stream/__init__.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Copyright 2017 The Kubernetes Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .stream import stream, portforward
|
50
kubernetes/base/stream/stream.py
Normal file
50
kubernetes/base/stream/stream.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
# Copyright 2018 The Kubernetes Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import functools
|
||||
|
||||
from . import ws_client
|
||||
|
||||
|
||||
def _websocket_request(websocket_request, force_kwargs, api_method, *args, **kwargs):
|
||||
"""Override the ApiClient.request method with an alternative websocket based
|
||||
method and call the supplied Kubernetes API method with that in place."""
|
||||
if force_kwargs:
|
||||
for kwarg, value in force_kwargs.items():
|
||||
kwargs[kwarg] = value
|
||||
api_client = api_method.__self__.api_client
|
||||
# old generated code's api client has config. new ones has configuration
|
||||
try:
|
||||
configuration = api_client.configuration
|
||||
except AttributeError:
|
||||
configuration = api_client.config
|
||||
prev_request = api_client.request
|
||||
binary = kwargs.pop('binary', False)
|
||||
try:
|
||||
api_client.request = functools.partial(websocket_request, configuration, binary=binary)
|
||||
out = api_method(*args, **kwargs)
|
||||
# The api_client insists on converting this to a string using its representation, so we have
|
||||
# to do this dance to strip it of the b' prefix and ' suffix, encode it byte-per-byte (latin1),
|
||||
# escape all of the unicode \x*'s, then encode it back byte-by-byte
|
||||
# However, if _preload_content=False is passed, then the entire WSClient is returned instead
|
||||
# of a response, and we want to leave it alone
|
||||
if binary and kwargs.get('_preload_content', True):
|
||||
out = out[2:-1].encode('latin1').decode('unicode_escape').encode('latin1')
|
||||
return out
|
||||
finally:
|
||||
api_client.request = prev_request
|
||||
|
||||
|
||||
stream = functools.partial(_websocket_request, ws_client.websocket_call, None)
|
||||
portforward = functools.partial(_websocket_request, ws_client.portforward_call, {'_preload_content':False})
|
571
kubernetes/base/stream/ws_client.py
Normal file
571
kubernetes/base/stream/ws_client.py
Normal file
|
@ -0,0 +1,571 @@
|
|||
# Copyright 2018 The Kubernetes Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import sys
|
||||
|
||||
from kubernetes.client.rest import ApiException, ApiValueError
|
||||
|
||||
import certifi
|
||||
import collections
|
||||
import select
|
||||
import socket
|
||||
import ssl
|
||||
import threading
|
||||
import time
|
||||
|
||||
import six
|
||||
import yaml
|
||||
|
||||
|
||||
from six.moves.urllib.parse import urlencode, urlparse, urlunparse
|
||||
from six import StringIO, BytesIO
|
||||
|
||||
from websocket import WebSocket, ABNF, enableTrace
|
||||
from base64 import urlsafe_b64decode
|
||||
from requests.utils import should_bypass_proxies
|
||||
|
||||
STDIN_CHANNEL = 0
|
||||
STDOUT_CHANNEL = 1
|
||||
STDERR_CHANNEL = 2
|
||||
ERROR_CHANNEL = 3
|
||||
RESIZE_CHANNEL = 4
|
||||
|
||||
class _IgnoredIO:
|
||||
def write(self, _x):
|
||||
pass
|
||||
|
||||
def getvalue(self):
|
||||
raise TypeError("Tried to read_all() from a WSClient configured to not capture. Did you mean `capture_all=True`?")
|
||||
|
||||
|
||||
class WSClient:
|
||||
def __init__(self, configuration, url, headers, capture_all, binary=False):
|
||||
"""A websocket client with support for channels.
|
||||
|
||||
Exec command uses different channels for different streams. for
|
||||
example, 0 is stdin, 1 is stdout and 2 is stderr. Some other API calls
|
||||
like port forwarding can forward different pods' streams to different
|
||||
channels.
|
||||
"""
|
||||
self._connected = False
|
||||
self._channels = {}
|
||||
self.binary = binary
|
||||
self.newline = '\n' if not self.binary else b'\n'
|
||||
if capture_all:
|
||||
self._all = StringIO() if not self.binary else BytesIO()
|
||||
else:
|
||||
self._all = _IgnoredIO()
|
||||
self.sock = create_websocket(configuration, url, headers)
|
||||
self._connected = True
|
||||
self._returncode = None
|
||||
|
||||
def peek_channel(self, channel, timeout=0):
|
||||
"""Peek a channel and return part of the input,
|
||||
empty string otherwise."""
|
||||
self.update(timeout=timeout)
|
||||
if channel in self._channels:
|
||||
return self._channels[channel]
|
||||
return ""
|
||||
|
||||
def read_channel(self, channel, timeout=0):
|
||||
"""Read data from a channel."""
|
||||
if channel not in self._channels:
|
||||
ret = self.peek_channel(channel, timeout)
|
||||
else:
|
||||
ret = self._channels[channel]
|
||||
if channel in self._channels:
|
||||
del self._channels[channel]
|
||||
return ret
|
||||
|
||||
def readline_channel(self, channel, timeout=None):
|
||||
"""Read a line from a channel."""
|
||||
if timeout is None:
|
||||
timeout = float("inf")
|
||||
start = time.time()
|
||||
while self.is_open() and time.time() - start < timeout:
|
||||
if channel in self._channels:
|
||||
data = self._channels[channel]
|
||||
if self.newline in data:
|
||||
index = data.find(self.newline)
|
||||
ret = data[:index]
|
||||
data = data[index+1:]
|
||||
if data:
|
||||
self._channels[channel] = data
|
||||
else:
|
||||
del self._channels[channel]
|
||||
return ret
|
||||
self.update(timeout=(timeout - time.time() + start))
|
||||
|
||||
def write_channel(self, channel, data):
|
||||
"""Write data to a channel."""
|
||||
# check if we're writing binary data or not
|
||||
binary = six.PY3 and type(data) == six.binary_type
|
||||
opcode = ABNF.OPCODE_BINARY if binary else ABNF.OPCODE_TEXT
|
||||
|
||||
channel_prefix = chr(channel)
|
||||
if binary:
|
||||
channel_prefix = six.binary_type(channel_prefix, "ascii")
|
||||
|
||||
payload = channel_prefix + data
|
||||
self.sock.send(payload, opcode=opcode)
|
||||
|
||||
def peek_stdout(self, timeout=0):
|
||||
"""Same as peek_channel with channel=1."""
|
||||
return self.peek_channel(STDOUT_CHANNEL, timeout=timeout)
|
||||
|
||||
def read_stdout(self, timeout=None):
|
||||
"""Same as read_channel with channel=1."""
|
||||
return self.read_channel(STDOUT_CHANNEL, timeout=timeout)
|
||||
|
||||
def readline_stdout(self, timeout=None):
|
||||
"""Same as readline_channel with channel=1."""
|
||||
return self.readline_channel(STDOUT_CHANNEL, timeout=timeout)
|
||||
|
||||
def peek_stderr(self, timeout=0):
|
||||
"""Same as peek_channel with channel=2."""
|
||||
return self.peek_channel(STDERR_CHANNEL, timeout=timeout)
|
||||
|
||||
def read_stderr(self, timeout=None):
|
||||
"""Same as read_channel with channel=2."""
|
||||
return self.read_channel(STDERR_CHANNEL, timeout=timeout)
|
||||
|
||||
def readline_stderr(self, timeout=None):
|
||||
"""Same as readline_channel with channel=2."""
|
||||
return self.readline_channel(STDERR_CHANNEL, timeout=timeout)
|
||||
|
||||
def read_all(self):
|
||||
"""Return buffered data received on stdout and stderr channels.
|
||||
This is useful for non-interactive call where a set of command passed
|
||||
to the API call and their result is needed after the call is concluded.
|
||||
Should be called after run_forever() or update()
|
||||
|
||||
TODO: Maybe we can process this and return a more meaningful map with
|
||||
channels mapped for each input.
|
||||
"""
|
||||
out = self._all.getvalue()
|
||||
self._all = self._all.__class__()
|
||||
self._channels = {}
|
||||
return out
|
||||
|
||||
def is_open(self):
|
||||
"""True if the connection is still alive."""
|
||||
return self._connected
|
||||
|
||||
def write_stdin(self, data):
|
||||
"""The same as write_channel with channel=0."""
|
||||
self.write_channel(STDIN_CHANNEL, data)
|
||||
|
||||
def update(self, timeout=0):
|
||||
"""Update channel buffers with at most one complete frame of input."""
|
||||
if not self.is_open():
|
||||
return
|
||||
if not self.sock.connected:
|
||||
self._connected = False
|
||||
return
|
||||
|
||||
# The options here are:
|
||||
# select.select() - this will work on most OS, however, it has a
|
||||
# limitation of only able to read fd numbers up to 1024.
|
||||
# i.e. does not scale well. This was the original
|
||||
# implementation.
|
||||
# select.poll() - this will work on most unix based OS, but not as
|
||||
# efficient as epoll. Will work for fd numbers above 1024.
|
||||
# select.epoll() - newest and most efficient way of polling.
|
||||
# However, only works on linux.
|
||||
if hasattr(select, "poll"):
|
||||
poll = select.poll()
|
||||
poll.register(self.sock.sock, select.POLLIN)
|
||||
if timeout is not None:
|
||||
timeout *= 1_000 # poll method uses milliseconds as the time unit
|
||||
r = poll.poll(timeout)
|
||||
poll.unregister(self.sock.sock)
|
||||
else:
|
||||
r, _, _ = select.select(
|
||||
(self.sock.sock, ), (), (), timeout)
|
||||
|
||||
if r:
|
||||
op_code, frame = self.sock.recv_data_frame(True)
|
||||
if op_code == ABNF.OPCODE_CLOSE:
|
||||
self._connected = False
|
||||
return
|
||||
elif op_code == ABNF.OPCODE_BINARY or op_code == ABNF.OPCODE_TEXT:
|
||||
data = frame.data
|
||||
if six.PY3 and not self.binary:
|
||||
data = data.decode("utf-8", "replace")
|
||||
if len(data) > 1:
|
||||
channel = data[0]
|
||||
if six.PY3 and not self.binary:
|
||||
channel = ord(channel)
|
||||
data = data[1:]
|
||||
if data:
|
||||
if channel in [STDOUT_CHANNEL, STDERR_CHANNEL]:
|
||||
# keeping all messages in the order they received
|
||||
# for non-blocking call.
|
||||
self._all.write(data)
|
||||
if channel not in self._channels:
|
||||
self._channels[channel] = data
|
||||
else:
|
||||
self._channels[channel] += data
|
||||
|
||||
def run_forever(self, timeout=None):
|
||||
"""Wait till connection is closed or timeout reached. Buffer any input
|
||||
received during this time."""
|
||||
if timeout:
|
||||
start = time.time()
|
||||
while self.is_open() and time.time() - start < timeout:
|
||||
self.update(timeout=(timeout - time.time() + start))
|
||||
else:
|
||||
while self.is_open():
|
||||
self.update(timeout=None)
|
||||
@property
|
||||
def returncode(self):
|
||||
"""
|
||||
The return code, A None value indicates that the process hasn't
|
||||
terminated yet.
|
||||
"""
|
||||
if self.is_open():
|
||||
return None
|
||||
else:
|
||||
if self._returncode is None:
|
||||
err = self.read_channel(ERROR_CHANNEL)
|
||||
err = yaml.safe_load(err)
|
||||
if err['status'] == "Success":
|
||||
self._returncode = 0
|
||||
else:
|
||||
self._returncode = int(err['details']['causes'][0]['message'])
|
||||
return self._returncode
|
||||
|
||||
def close(self, **kwargs):
|
||||
"""
|
||||
close websocket connection.
|
||||
"""
|
||||
self._connected = False
|
||||
if self.sock:
|
||||
self.sock.close(**kwargs)
|
||||
|
||||
|
||||
WSResponse = collections.namedtuple('WSResponse', ['data'])
|
||||
|
||||
|
||||
class PortForward:
|
||||
def __init__(self, websocket, ports):
|
||||
"""A websocket client with support for port forwarding.
|
||||
|
||||
Port Forward command sends on 2 channels per port, a read/write
|
||||
data channel and a read only error channel. Both channels are sent an
|
||||
initial frame containing the port number that channel is associated with.
|
||||
"""
|
||||
|
||||
self.websocket = websocket
|
||||
self.local_ports = {}
|
||||
for ix, port_number in enumerate(ports):
|
||||
self.local_ports[port_number] = self._Port(ix, port_number)
|
||||
# There is a thread run per PortForward instance which performs the translation between the
|
||||
# raw socket data sent by the python application and the websocket protocol. This thread
|
||||
# terminates after either side has closed all ports, and after flushing all pending data.
|
||||
proxy = threading.Thread(
|
||||
name="Kubernetes port forward proxy: %s" % ', '.join([str(port) for port in ports]),
|
||||
target=self._proxy
|
||||
)
|
||||
proxy.daemon = True
|
||||
proxy.start()
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
return self.websocket.connected
|
||||
|
||||
def socket(self, port_number):
|
||||
if port_number not in self.local_ports:
|
||||
raise ValueError("Invalid port number")
|
||||
return self.local_ports[port_number].socket
|
||||
|
||||
def error(self, port_number):
|
||||
if port_number not in self.local_ports:
|
||||
raise ValueError("Invalid port number")
|
||||
return self.local_ports[port_number].error
|
||||
|
||||
def close(self):
|
||||
for port in self.local_ports.values():
|
||||
port.socket.close()
|
||||
|
||||
class _Port:
|
||||
def __init__(self, ix, port_number):
|
||||
# The remote port number
|
||||
self.port_number = port_number
|
||||
# The websocket channel byte number for this port
|
||||
self.channel = six.int2byte(ix * 2)
|
||||
# A socket pair is created to provide a means of translating the data flow
|
||||
# between the python application and the kubernetes websocket. The self.python
|
||||
# half of the socket pair is used by the _proxy method to receive and send data
|
||||
# to the running python application.
|
||||
s, self.python = socket.socketpair()
|
||||
# The self.socket half of the pair is used by the python application to send
|
||||
# and receive data to the eventual pod port. It is wrapped in the _Socket class
|
||||
# because a socket pair is an AF_UNIX socket, not a AF_INET socket. This allows
|
||||
# intercepting setting AF_INET socket options that would error against an AF_UNIX
|
||||
# socket.
|
||||
self.socket = self._Socket(s)
|
||||
# Data accumulated from the websocket to be sent to the python application.
|
||||
self.data = b''
|
||||
# All data sent from kubernetes on the port error channel.
|
||||
self.error = None
|
||||
|
||||
class _Socket:
|
||||
def __init__(self, socket):
|
||||
self._socket = socket
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._socket, name)
|
||||
|
||||
def setsockopt(self, level, optname, value):
|
||||
# The following socket option is not valid with a socket created from socketpair,
|
||||
# and is set by the http.client.HTTPConnection.connect method.
|
||||
if level == socket.IPPROTO_TCP and optname == socket.TCP_NODELAY:
|
||||
return
|
||||
self._socket.setsockopt(level, optname, value)
|
||||
|
||||
# Proxy all socket data between the python code and the kubernetes websocket.
|
||||
def _proxy(self):
|
||||
channel_ports = []
|
||||
channel_initialized = []
|
||||
local_ports = {}
|
||||
for port in self.local_ports.values():
|
||||
# Setup the data channel for this port number
|
||||
channel_ports.append(port)
|
||||
channel_initialized.append(False)
|
||||
# Setup the error channel for this port number
|
||||
channel_ports.append(port)
|
||||
channel_initialized.append(False)
|
||||
port.python.setblocking(True)
|
||||
local_ports[port.python] = port
|
||||
# The data to send on the websocket socket
|
||||
kubernetes_data = b''
|
||||
while True:
|
||||
rlist = [] # List of sockets to read from
|
||||
wlist = [] # List of sockets to write to
|
||||
if self.websocket.connected:
|
||||
rlist.append(self.websocket)
|
||||
if kubernetes_data:
|
||||
wlist.append(self.websocket)
|
||||
local_all_closed = True
|
||||
for port in self.local_ports.values():
|
||||
if port.python.fileno() != -1:
|
||||
if self.websocket.connected:
|
||||
rlist.append(port.python)
|
||||
if port.data:
|
||||
wlist.append(port.python)
|
||||
local_all_closed = False
|
||||
else:
|
||||
if port.data:
|
||||
wlist.append(port.python)
|
||||
local_all_closed = False
|
||||
else:
|
||||
port.python.close()
|
||||
if local_all_closed and not (self.websocket.connected and kubernetes_data):
|
||||
self.websocket.close()
|
||||
return
|
||||
r, w, _ = select.select(rlist, wlist, [])
|
||||
for sock in r:
|
||||
if sock == self.websocket:
|
||||
pending = True
|
||||
while pending:
|
||||
opcode, frame = self.websocket.recv_data_frame(True)
|
||||
if opcode == ABNF.OPCODE_BINARY:
|
||||
if not frame.data:
|
||||
raise RuntimeError("Unexpected frame data size")
|
||||
channel = six.byte2int(frame.data)
|
||||
if channel >= len(channel_ports):
|
||||
raise RuntimeError("Unexpected channel number: %s" % channel)
|
||||
port = channel_ports[channel]
|
||||
if channel_initialized[channel]:
|
||||
if channel % 2:
|
||||
if port.error is None:
|
||||
port.error = ''
|
||||
port.error += frame.data[1:].decode()
|
||||
port.python.close()
|
||||
else:
|
||||
port.data += frame.data[1:]
|
||||
else:
|
||||
if len(frame.data) != 3:
|
||||
raise RuntimeError(
|
||||
"Unexpected initial channel frame data size"
|
||||
)
|
||||
port_number = six.byte2int(frame.data[1:2]) + (six.byte2int(frame.data[2:3]) * 256)
|
||||
if port_number != port.port_number:
|
||||
raise RuntimeError(
|
||||
"Unexpected port number in initial channel frame: %s" % port_number
|
||||
)
|
||||
channel_initialized[channel] = True
|
||||
elif opcode not in (ABNF.OPCODE_PING, ABNF.OPCODE_PONG, ABNF.OPCODE_CLOSE):
|
||||
raise RuntimeError("Unexpected websocket opcode: %s" % opcode)
|
||||
if not (isinstance(self.websocket.sock, ssl.SSLSocket) and self.websocket.sock.pending()):
|
||||
pending = False
|
||||
else:
|
||||
port = local_ports[sock]
|
||||
if port.python.fileno() != -1:
|
||||
data = port.python.recv(1024 * 1024)
|
||||
if data:
|
||||
kubernetes_data += ABNF.create_frame(
|
||||
port.channel + data,
|
||||
ABNF.OPCODE_BINARY,
|
||||
).format()
|
||||
else:
|
||||
port.python.close()
|
||||
for sock in w:
|
||||
if sock == self.websocket:
|
||||
sent = self.websocket.sock.send(kubernetes_data)
|
||||
kubernetes_data = kubernetes_data[sent:]
|
||||
else:
|
||||
port = local_ports[sock]
|
||||
if port.python.fileno() != -1:
|
||||
sent = port.python.send(port.data)
|
||||
port.data = port.data[sent:]
|
||||
|
||||
|
||||
def get_websocket_url(url, query_params=None):
|
||||
parsed_url = urlparse(url)
|
||||
parts = list(parsed_url)
|
||||
if parsed_url.scheme == 'http':
|
||||
parts[0] = 'ws'
|
||||
elif parsed_url.scheme == 'https':
|
||||
parts[0] = 'wss'
|
||||
if query_params:
|
||||
query = []
|
||||
for key, value in query_params:
|
||||
if key == 'command' and isinstance(value, list):
|
||||
for command in value:
|
||||
query.append((key, command))
|
||||
else:
|
||||
query.append((key, value))
|
||||
if query:
|
||||
parts[4] = urlencode(query)
|
||||
return urlunparse(parts)
|
||||
|
||||
|
||||
def create_websocket(configuration, url, headers=None):
|
||||
enableTrace(False)
|
||||
|
||||
# We just need to pass the Authorization, ignore all the other
|
||||
# http headers we get from the generated code
|
||||
header = []
|
||||
if headers and 'authorization' in headers:
|
||||
header.append("authorization: %s" % headers['authorization'])
|
||||
if headers and 'sec-websocket-protocol' in headers:
|
||||
header.append("sec-websocket-protocol: %s" %
|
||||
headers['sec-websocket-protocol'])
|
||||
else:
|
||||
header.append("sec-websocket-protocol: v4.channel.k8s.io")
|
||||
|
||||
if url.startswith('wss://') and configuration.verify_ssl:
|
||||
ssl_opts = {
|
||||
'cert_reqs': ssl.CERT_REQUIRED,
|
||||
'ca_certs': configuration.ssl_ca_cert or certifi.where(),
|
||||
}
|
||||
if configuration.assert_hostname is not None:
|
||||
ssl_opts['check_hostname'] = configuration.assert_hostname
|
||||
else:
|
||||
ssl_opts = {'cert_reqs': ssl.CERT_NONE}
|
||||
|
||||
if configuration.cert_file:
|
||||
ssl_opts['certfile'] = configuration.cert_file
|
||||
if configuration.key_file:
|
||||
ssl_opts['keyfile'] = configuration.key_file
|
||||
if configuration.tls_server_name:
|
||||
ssl_opts['server_hostname'] = configuration.tls_server_name
|
||||
|
||||
websocket = WebSocket(sslopt=ssl_opts, skip_utf8_validation=False)
|
||||
connect_opt = {
|
||||
'header': header
|
||||
}
|
||||
|
||||
if configuration.proxy or configuration.proxy_headers:
|
||||
connect_opt = websocket_proxycare(connect_opt, configuration, url, headers)
|
||||
|
||||
websocket.connect(url, **connect_opt)
|
||||
return websocket
|
||||
|
||||
def websocket_proxycare(connect_opt, configuration, url, headers):
|
||||
""" An internal function to be called in api-client when a websocket
|
||||
create is requested.
|
||||
"""
|
||||
if configuration.no_proxy:
|
||||
connect_opt.update({ 'http_no_proxy': configuration.no_proxy.split(',') })
|
||||
|
||||
if configuration.proxy:
|
||||
proxy_url = urlparse(configuration.proxy)
|
||||
connect_opt.update({'http_proxy_host': proxy_url.hostname, 'http_proxy_port': proxy_url.port})
|
||||
if configuration.proxy_headers:
|
||||
for key,value in configuration.proxy_headers.items():
|
||||
if key == 'proxy-authorization' and value.startswith('Basic'):
|
||||
b64value = value.split()[1]
|
||||
auth = urlsafe_b64decode(b64value).decode().split(':')
|
||||
connect_opt.update({'http_proxy_auth': (auth[0], auth[1]) })
|
||||
return(connect_opt)
|
||||
|
||||
|
||||
def websocket_call(configuration, _method, url, **kwargs):
|
||||
"""An internal function to be called in api-client when a websocket
|
||||
connection is required. method, url, and kwargs are the parameters of
|
||||
apiClient.request method."""
|
||||
|
||||
url = get_websocket_url(url, kwargs.get("query_params"))
|
||||
headers = kwargs.get("headers")
|
||||
_request_timeout = kwargs.get("_request_timeout", 60)
|
||||
_preload_content = kwargs.get("_preload_content", True)
|
||||
capture_all = kwargs.get("capture_all", True)
|
||||
binary = kwargs.get('binary', False)
|
||||
try:
|
||||
client = WSClient(configuration, url, headers, capture_all, binary=binary)
|
||||
if not _preload_content:
|
||||
return client
|
||||
client.run_forever(timeout=_request_timeout)
|
||||
all = client.read_all()
|
||||
if binary:
|
||||
return WSResponse(all)
|
||||
else:
|
||||
return WSResponse('%s' % ''.join(all))
|
||||
except (Exception, KeyboardInterrupt, SystemExit) as e:
|
||||
raise ApiException(status=0, reason=str(e))
|
||||
|
||||
|
||||
def portforward_call(configuration, _method, url, **kwargs):
|
||||
"""An internal function to be called in api-client when a websocket
|
||||
connection is required for port forwarding. args and kwargs are the
|
||||
parameters of apiClient.request method."""
|
||||
|
||||
query_params = kwargs.get("query_params")
|
||||
|
||||
ports = []
|
||||
for param, value in query_params:
|
||||
if param == 'ports':
|
||||
for port in value.split(','):
|
||||
try:
|
||||
port_number = int(port)
|
||||
except ValueError:
|
||||
raise ApiValueError("Invalid port number: %s" % port)
|
||||
if not (0 < port_number < 65536):
|
||||
raise ApiValueError("Port number must be between 0 and 65536: %s" % port)
|
||||
if port_number in ports:
|
||||
raise ApiValueError("Duplicate port numbers: %s" % port)
|
||||
ports.append(port_number)
|
||||
if not ports:
|
||||
raise ApiValueError("Missing required parameter `ports`")
|
||||
|
||||
url = get_websocket_url(url, query_params)
|
||||
headers = kwargs.get("headers")
|
||||
|
||||
try:
|
||||
websocket = create_websocket(configuration, url, headers)
|
||||
return PortForward(websocket, ports)
|
||||
except (Exception, KeyboardInterrupt, SystemExit) as e:
|
||||
raise ApiException(status=0, reason=str(e))
|
76
kubernetes/base/stream/ws_client_test.py
Normal file
76
kubernetes/base/stream/ws_client_test.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
# Copyright 2018 The Kubernetes Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import unittest
|
||||
|
||||
from .ws_client import get_websocket_url
|
||||
from .ws_client import websocket_proxycare
|
||||
from kubernetes.client.configuration import Configuration
|
||||
|
||||
try:
|
||||
import urllib3
|
||||
urllib3.disable_warnings()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def dictval(dict, key, default=None):
|
||||
try:
|
||||
val = dict[key]
|
||||
except KeyError:
|
||||
val = default
|
||||
return val
|
||||
|
||||
class WSClientTest(unittest.TestCase):
|
||||
|
||||
def test_websocket_client(self):
|
||||
for url, ws_url in [
|
||||
('http://localhost/api', 'ws://localhost/api'),
|
||||
('https://localhost/api', 'wss://localhost/api'),
|
||||
('https://domain.com/api', 'wss://domain.com/api'),
|
||||
('https://api.domain.com/api', 'wss://api.domain.com/api'),
|
||||
('http://api.domain.com', 'ws://api.domain.com'),
|
||||
('https://api.domain.com', 'wss://api.domain.com'),
|
||||
('http://api.domain.com/', 'ws://api.domain.com/'),
|
||||
('https://api.domain.com/', 'wss://api.domain.com/'),
|
||||
]:
|
||||
self.assertEqual(get_websocket_url(url), ws_url)
|
||||
|
||||
def test_websocket_proxycare(self):
|
||||
for proxy, idpass, no_proxy, expect_host, expect_port, expect_auth, expect_noproxy in [
|
||||
( None, None, None, None, None, None, None ),
|
||||
( 'http://proxy.example.com:8080/', None, None, 'proxy.example.com', 8080, None, None ),
|
||||
( 'http://proxy.example.com:8080/', 'user:pass', None, 'proxy.example.com', 8080, ('user','pass'), None),
|
||||
( 'http://proxy.example.com:8080/', 'user:pass', '', 'proxy.example.com', 8080, ('user','pass'), None),
|
||||
( 'http://proxy.example.com:8080/', 'user:pass', '*', 'proxy.example.com', 8080, ('user','pass'), ['*']),
|
||||
( 'http://proxy.example.com:8080/', 'user:pass', '.example.com', 'proxy.example.com', 8080, ('user','pass'), ['.example.com']),
|
||||
( 'http://proxy.example.com:8080/', 'user:pass', 'localhost,.local,.example.com', 'proxy.example.com', 8080, ('user','pass'), ['localhost','.local','.example.com']),
|
||||
]:
|
||||
# setup input
|
||||
config = Configuration()
|
||||
if proxy is not None:
|
||||
setattr(config, 'proxy', proxy)
|
||||
if idpass is not None:
|
||||
setattr(config, 'proxy_headers', urllib3.util.make_headers(proxy_basic_auth=idpass))
|
||||
if no_proxy is not None:
|
||||
setattr(config, 'no_proxy', no_proxy)
|
||||
# setup done
|
||||
# test starts
|
||||
connect_opt = websocket_proxycare( {}, config, None, None)
|
||||
self.assertEqual( dictval(connect_opt,'http_proxy_host'), expect_host)
|
||||
self.assertEqual( dictval(connect_opt,'http_proxy_port'), expect_port)
|
||||
self.assertEqual( dictval(connect_opt,'http_proxy_auth'), expect_auth)
|
||||
self.assertEqual( dictval(connect_opt,'http_no_proxy'), expect_noproxy)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Reference in a new issue