Skip to content
Merged
2 changes: 1 addition & 1 deletion .github/workflows/e2e-master.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
cluster_name: kubernetes-python-e2e-master-${{ matrix.python-version }}
# The kind version to be used to spin the cluster up
# this needs to be updated whenever a new Kind version is released
version: v0.17.0
version: v0.31.0
# Update the config here whenever a new client snapshot is performed
# This would eventually point to cluster with the latest Kubernetes version
# as we sync with Kubernetes upstream
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/e2e-release-35.0.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
cluster_name: kubernetes-python-e2e-release-35.0-${{ matrix.python-version }}
# The kind version to be used to spin the cluster up
# this needs to be updated whenever a new Kind version is released
version: v0.17.0
version: v0.31.0
# Update the config here whenever a new client snapshot is performed
# This would eventually point to cluster with the latest Kubernetes version
# as we sync with Kubernetes upstream
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# v36.0.0b1

Kubernetes API Version: v1.36.1

### Deprecation
- Support new exec v5 websocket subprotocol (#2486, @aojea)

# v36.0.0a3

Kubernetes API Version: v1.36.0
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ supported versions of Kubernetes clusters.
- [client 33.y.z](https://pypi.org/project/kubernetes/33.1.0/): Kubernetes 1.32 or below (+-), Kubernetes 1.33 (✓), Kubernetes 1.34 or above (+-)
- [client 34.y.z](https://pypi.org/project/kubernetes/34.1.0/): Kubernetes 1.33 or below (+-), Kubernetes 1.34 (✓), Kubernetes 1.35 or above (+-)
- [client 35.y.z](https://pypi.org/project/kubernetes/35.0.0/): Kubernetes 1.34 or below (+-), Kubernetes 1.35 (✓), Kubernetes 1.36 or above (+-)
- [client 36.y.z](https://pypi.org/project/kubernetes/36.0.0a3/): Kubernetes 1.35 or below (+-), Kubernetes 1.36 (✓), Kubernetes 1.37 or above (+-)
- [client 36.y.z](https://pypi.org/project/kubernetes/36.0.0b1/): Kubernetes 1.35 or below (+-), Kubernetes 1.36 (✓), Kubernetes 1.37 or above (+-)


> See [here](#homogenizing-the-kubernetes-python-client-versions) for an explanation of why there is no v13-v16 release.
Expand Down
2 changes: 1 addition & 1 deletion kubernetes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ No description provided (generated by Openapi Generator https://github.com/opena
This Python package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:

- API version: release-1.36
- Package version: 36.0.0a3
- Package version: 36.0.0b1
- Build package: org.openapitools.codegen.languages.PythonLegacyClientCodegen

## Requirements.
Expand Down
2 changes: 1 addition & 1 deletion kubernetes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

__project__ = 'kubernetes'
# The version is auto-updated. Please do not edit.
__version__ = "36.0.0a3"
__version__ = "36.0.0b1"

from . import client
from . import config
Expand Down
60 changes: 53 additions & 7 deletions kubernetes/base/stream/ws_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
STDERR_CHANNEL = 2
ERROR_CHANNEL = 3
RESIZE_CHANNEL = 4
CLOSE_CHANNEL = 255

V4_CHANNEL_PROTOCOL = "v4.channel.k8s.io"
V5_CHANNEL_PROTOCOL = "v5.channel.k8s.io"

class _IgnoredIO:
def write(self, _x):
Expand All @@ -59,26 +63,40 @@ def __init__(self, configuration, url, headers, capture_all, binary=False):
"""
self._connected = False
self._channels = {}
self._closed_channels = set()
self.subprotocol = None
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.subprotocol = getattr(self.sock, 'subprotocol', None)
if not self.subprotocol and self.sock:
headers_dict = self.sock.getheaders()
if headers_dict:
for k, v in headers_dict.items():
if k.lower() == 'sec-websocket-protocol':
self.subprotocol = v
break
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."""
if channel in self._closed_channels and channel not in self._channels:
return b"" if self.binary else ""
self.update(timeout=timeout)
if channel in self._channels:
return self._channels[channel]
return ""
return b"" if self.binary else ""

def read_channel(self, channel, timeout=0):
"""Read data from a channel."""
if channel in self._closed_channels and channel not in self._channels:
return b"" if self.binary else ""
if channel not in self._channels:
ret = self.peek_channel(channel, timeout)
else:
Expand All @@ -93,6 +111,7 @@ def readline_channel(self, channel, timeout=None):
timeout = float("inf")
start = time.time()
while self.is_open() and time.time() - start < timeout:
# Always try to drain the channel first
if channel in self._channels:
data = self._channels[channel]
if self.newline in data:
Expand All @@ -104,6 +123,14 @@ def readline_channel(self, channel, timeout=None):
else:
del self._channels[channel]
return ret

if channel in self._closed_channels:
if channel in self._channels:
ret = self._channels[channel]
del self._channels[channel]
return ret
return b"" if self.binary else ""

self.update(timeout=(timeout - time.time() + start))

def write_channel(self, channel, data):
Expand All @@ -119,6 +146,14 @@ def write_channel(self, channel, data):
payload = channel_prefix + data
self.sock.send(payload, opcode=opcode)

def close_channel(self, channel):
"""Close a channel (v5 protocol only)."""
if self.subprotocol != V5_CHANNEL_PROTOCOL:
return
data = bytes([CLOSE_CHANNEL, channel])
self.sock.send(data, opcode=ABNF.OPCODE_BINARY)
self._closed_channels.add(channel)

def peek_stdout(self, timeout=0):
"""Same as peek_channel with channel=1."""
return self.peek_channel(STDOUT_CHANNEL, timeout=timeout)
Expand Down Expand Up @@ -200,13 +235,24 @@ def update(self, timeout=0):
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:
if len(data) > 0:
# Parse channel from raw bytes to support v5 CLOSE signal AND avoid charset issues
channel = data[0]
if six.PY3 and not self.binary:
channel = ord(channel)
# In Py3, iterating bytes gives int, but indexing bytes gives int.
# websocket-client frame.data might be bytes.

if channel == CLOSE_CHANNEL and self.subprotocol == V5_CHANNEL_PROTOCOL: # v5 CLOSE
if len(data) > 1:
# data[1] is already int in Py3 bytes
close_chan = data[1]
self._closed_channels.add(close_chan)
return

data = data[1:]
# Decode data if expected text
if not self.binary:
data = data.decode("utf-8", "replace")

if data:
if channel in [STDOUT_CHANNEL, STDERR_CHANNEL]:
# keeping all messages in the order they received
Expand Down Expand Up @@ -476,7 +522,7 @@ def create_websocket(configuration, url, headers=None):
header.append("sec-websocket-protocol: %s" %
headers['sec-websocket-protocol'])
else:
header.append("sec-websocket-protocol: v4.channel.k8s.io")
header.append("sec-websocket-protocol: %s,%s" % (V5_CHANNEL_PROTOCOL, V4_CHANNEL_PROTOCOL))

if url.startswith('wss://') and configuration.verify_ssl:
ssl_opts = {
Expand Down
Loading