From 077da00774ace5a184e9dcfe560868e219edf1e8 Mon Sep 17 00:00:00 2001 From: chala2001 Date: Sat, 29 Aug 2026 14:38:25 +0530 Subject: [PATCH 01/14] Add an opt-in TCP keepalive option to the sync client Long lived requests such as watches are dropped silently when an idle proxy or load balancer closes the connection, because the client never sends anything on it. Setting keep_alive on the Configuration now asks the kernel for the same keepalive timings client-go dials with: probe after 30s idle, then every 15s, giving up after 9 probes. socket_options still wins if it is set, so existing callers are unaffected. --- kubernetes/client/configuration.py | 9 ++ kubernetes/client/rest.py | 3 + kubernetes/utils/__init__.py | 1 + kubernetes/utils/keepalive.py | 74 +++++++++++++++ kubernetes/utils/keepalive_test.py | 140 +++++++++++++++++++++++++++++ scripts/keepalive_patch.diff | 39 ++++++++ scripts/update-client.sh | 3 + 7 files changed, 269 insertions(+) create mode 100644 kubernetes/utils/keepalive.py create mode 100644 kubernetes/utils/keepalive_test.py create mode 100644 scripts/keepalive_patch.diff diff --git a/kubernetes/client/configuration.py b/kubernetes/client/configuration.py index ecb0de48b5..4caa1af87b 100644 --- a/kubernetes/client/configuration.py +++ b/kubernetes/client/configuration.py @@ -387,6 +387,15 @@ def __init__( self.socket_options = socket_options """Options to pass down to the underlying urllib3 socket """ + self.keep_alive = False + """Enable TCP keepalive on the underlying urllib3 sockets. + + Long lived requests such as watches are otherwise dropped + silently by an idle proxy or load balancer. When enabled, the + client asks the kernel for the same keepalive timings client-go + uses. Ignored if ``socket_options`` is set, which takes + precedence. + """ self.datetime_format = datetime_format """datetime format diff --git a/kubernetes/client/rest.py b/kubernetes/client/rest.py index 78d79207e7..1a1015d75e 100644 --- a/kubernetes/client/rest.py +++ b/kubernetes/client/rest.py @@ -27,6 +27,7 @@ on_retry_after_error, retry_after_backoff, ) +from kubernetes.utils.keepalive import tcp_keepalive_socket_options from kubernetes.client.exceptions import ApiException, ApiValueError SUPPORTED_SOCKS_PROXIES = {"socks5", "socks5h", "socks4", "socks4a"} @@ -160,6 +161,8 @@ def __init__(self, configuration) -> None: if configuration.socket_options is not None: pool_args['socket_options'] = configuration.socket_options + elif getattr(configuration, 'keep_alive', False): + pool_args['socket_options'] = tcp_keepalive_socket_options() if configuration.connection_pool_maxsize is not None: pool_args['maxsize'] = configuration.connection_pool_maxsize diff --git a/kubernetes/utils/__init__.py b/kubernetes/utils/__init__.py index 681123a57c..4b8c07f312 100644 --- a/kubernetes/utils/__init__.py +++ b/kubernetes/utils/__init__.py @@ -25,3 +25,4 @@ on_retry_after_error, retry_after_backoff, retry_after_max_retries, retry_on_conflict, retry_after_seconds) +from .keepalive import tcp_keepalive_socket_options diff --git a/kubernetes/utils/keepalive.py b/kubernetes/utils/keepalive.py new file mode 100644 index 0000000000..0e54a43ea6 --- /dev/null +++ b/kubernetes/utils/keepalive.py @@ -0,0 +1,74 @@ +# Copyright 2026 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 socket +from typing import List, Tuple + +import urllib3 + + +# client-go dials the API server with a 30 second keepalive: +# https://github.com/kubernetes/client-go/blob/master/transport/cache.go +# Go folds that single duration into the idle time and leaves the probe +# interval and count at its own defaults, 15 seconds and 9 probes: +# https://github.com/golang/go/blob/master/src/net/tcpsock.go +# https://github.com/golang/go/blob/master/src/net/dial.go +DEFAULT_IDLE = 30 +DEFAULT_INTERVAL = 15 +DEFAULT_COUNT = 9 + +SocketOptions = List[Tuple[int, int, int]] + + +def tcp_keepalive_socket_options( + idle: int = DEFAULT_IDLE, + interval: int = DEFAULT_INTERVAL, + count: int = DEFAULT_COUNT, +) -> SocketOptions: + """Build urllib3 socket options that enable TCP keepalive. + + The defaults match what client-go asks the kernel for, so an idle + watch is probed after ``idle`` seconds and dropped after ``count`` + unanswered probes ``interval`` seconds apart. + + The returned list starts from ``urllib3``'s own default socket + options, which disable Nagle's algorithm. urllib3 replaces its + defaults with whatever list it is given rather than merging, so + building on them keeps that behaviour. + + Options the platform does not define are left out: macOS spells the + idle time ``TCP_KEEPALIVE``, and Windows only grew the idle and + interval options in Windows 10 1709. + """ + + for name, value in (('idle', idle), ('interval', interval), + ('count', count)): + if value < 1: + raise ValueError('%s must be at least 1' % name) + + options = list(urllib3.connection.HTTPConnection.default_socket_options) + options.append((socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)) + + if hasattr(socket, 'TCP_KEEPIDLE'): + options.append((socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, idle)) + elif hasattr(socket, 'TCP_KEEPALIVE'): + options.append((socket.IPPROTO_TCP, socket.TCP_KEEPALIVE, idle)) + + if hasattr(socket, 'TCP_KEEPINTVL'): + options.append((socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, interval)) + + if hasattr(socket, 'TCP_KEEPCNT'): + options.append((socket.IPPROTO_TCP, socket.TCP_KEEPCNT, count)) + + return options diff --git a/kubernetes/utils/keepalive_test.py b/kubernetes/utils/keepalive_test.py new file mode 100644 index 0000000000..005808fb83 --- /dev/null +++ b/kubernetes/utils/keepalive_test.py @@ -0,0 +1,140 @@ +# Copyright 2026 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 socket +import types +import unittest +from unittest import mock + +import urllib3 + +from kubernetes.client import Configuration +from kubernetes.client.rest import RESTClientObject +from kubernetes.utils import keepalive +from kubernetes.utils.keepalive import tcp_keepalive_socket_options + + +def fake_socket_module(**names): + """A stand-in for the socket module exposing only the given names.""" + + defaults = { + 'SOL_SOCKET': socket.SOL_SOCKET, + 'SO_KEEPALIVE': socket.SO_KEEPALIVE, + 'IPPROTO_TCP': socket.IPPROTO_TCP, + } + defaults.update(names) + return types.SimpleNamespace(**defaults) + + +class TestTcpKeepaliveSocketOptions(unittest.TestCase): + + def test_defaults_match_client_go(self): + options = tcp_keepalive_socket_options() + + self.assertIn((socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), options) + self.assertIn( + (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 30), options) + self.assertIn( + (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 15), options) + self.assertIn((socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 9), options) + + def test_keeps_the_urllib3_defaults(self): + options = tcp_keepalive_socket_options() + + defaults = urllib3.connection.HTTPConnection.default_socket_options + for default in defaults: + self.assertIn(default, options) + + def test_custom_timings(self): + options = tcp_keepalive_socket_options(idle=5, interval=2, count=3) + + self.assertIn((socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 5), options) + self.assertIn((socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 2), options) + self.assertIn((socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3), options) + + def test_timings_must_be_positive(self): + for kwargs in ({'idle': 0}, {'interval': 0}, {'count': 0}): + with self.assertRaises(ValueError): + tcp_keepalive_socket_options(**kwargs) + + def test_options_are_setsockopt_triples(self): + for option in tcp_keepalive_socket_options(): + self.assertEqual(3, len(option)) + for item in option: + self.assertIsInstance(item, int) + + def test_options_apply_to_a_socket(self): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + for level, name, value in tcp_keepalive_socket_options(): + sock.setsockopt(level, name, value) + + self.assertEqual( + 1, sock.getsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE)) + self.assertEqual( + 30, sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE)) + + def test_falls_back_to_tcp_keepalive_on_macos(self): + macos = fake_socket_module( + TCP_KEEPALIVE=0x10, + TCP_KEEPINTVL=socket.TCP_KEEPINTVL, + TCP_KEEPCNT=socket.TCP_KEEPCNT, + ) + with mock.patch.object(keepalive, 'socket', macos): + options = tcp_keepalive_socket_options() + + self.assertIn((socket.IPPROTO_TCP, 0x10, 30), options) + + def test_skips_options_the_platform_lacks(self): + with mock.patch.object(keepalive, 'socket', fake_socket_module()): + options = tcp_keepalive_socket_options() + + self.assertIn((socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), options) + defaults = urllib3.connection.HTTPConnection.default_socket_options + self.assertEqual(len(defaults) + 1, len(options)) + + +class TestConfigurationKeepAlive(unittest.TestCase): + + def pool_socket_options(self, configuration): + rest_client = RESTClientObject(configuration) + return rest_client.pool_manager.connection_pool_kw.get( + 'socket_options') + + def test_off_by_default(self): + configuration = Configuration() + + self.assertFalse(configuration.keep_alive) + self.assertIsNone(self.pool_socket_options(configuration)) + + def test_enabled(self): + configuration = Configuration() + configuration.keep_alive = True + + self.assertEqual( + tcp_keepalive_socket_options(), + self.pool_socket_options(configuration)) + + def test_socket_options_win(self): + configuration = Configuration() + configuration.keep_alive = True + configuration.socket_options = [ + (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)] + + self.assertEqual( + configuration.socket_options, + self.pool_socket_options(configuration)) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/keepalive_patch.diff b/scripts/keepalive_patch.diff new file mode 100644 index 0000000000..1583fde05c --- /dev/null +++ b/scripts/keepalive_patch.diff @@ -0,0 +1,39 @@ +diff --git a/kubernetes/client/configuration.py b/kubernetes/client/configuration.py +--- a/kubernetes/client/configuration.py ++++ b/kubernetes/client/configuration.py +@@ -387,6 +387,15 @@ + self.socket_options = socket_options + """Options to pass down to the underlying urllib3 socket + """ ++ self.keep_alive = False ++ """Enable TCP keepalive on the underlying urllib3 sockets. ++ ++ Long lived requests such as watches are otherwise dropped ++ silently by an idle proxy or load balancer. When enabled, the ++ client asks the kernel for the same keepalive timings client-go ++ uses. Ignored if ``socket_options`` is set, which takes ++ precedence. ++ """ + + self.datetime_format = datetime_format + """datetime format +diff --git a/kubernetes/client/rest.py b/kubernetes/client/rest.py +--- a/kubernetes/client/rest.py ++++ b/kubernetes/client/rest.py +@@ -27,6 +27,7 @@ + on_retry_after_error, + retry_after_backoff, + ) ++from kubernetes.utils.keepalive import tcp_keepalive_socket_options + from kubernetes.client.exceptions import ApiException, ApiValueError + + SUPPORTED_SOCKS_PROXIES = {"socks5", "socks5h", "socks4", "socks4a"} +@@ -160,6 +161,8 @@ + + if configuration.socket_options is not None: + pool_args['socket_options'] = configuration.socket_options ++ elif getattr(configuration, 'keep_alive', False): ++ pool_args['socket_options'] = tcp_keepalive_socket_options() + + if configuration.connection_pool_maxsize is not None: + pool_args['maxsize'] = configuration.connection_pool_maxsize diff --git a/scripts/update-client.sh b/scripts/update-client.sh index e128e8454c..102db9a1b7 100755 --- a/scripts/update-client.sh +++ b/scripts/update-client.sh @@ -65,6 +65,9 @@ git apply "${SCRIPT_ROOT}/rest_client_patch.diff" echo ">>> restoring Kubernetes client-go retry integration..." git apply "${SCRIPT_ROOT}/client_go_retry_patch.diff" +echo ">>> restoring Kubernetes TCP keepalive option..." +git apply "${SCRIPT_ROOT}/keepalive_patch.diff" + echo ">>> updating version information..." sed -i'' "s/^CLIENT_VERSION = .*/CLIENT_VERSION = \\\"${CLIENT_VERSION}\\\"/" "${SCRIPT_ROOT}/../setup.py" sed -i'' "s/^__version__ = .*/__version__ = \\\"${CLIENT_VERSION}\\\"/" "${CLIENT_ROOT}/__init__.py" From 1ff9908fa5bfcdd29813c8b35277fab8a88dc70d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 03:02:30 +0000 Subject: [PATCH 02/14] Bump helm/kind-action from 1.14.0 to 1.15.0 Bumps [helm/kind-action](https://github.com/helm/kind-action) from 1.14.0 to 1.15.0. - [Release notes](https://github.com/helm/kind-action/releases) - [Commits](https://github.com/helm/kind-action/compare/ef37e7f390d99f746eb8b610417061a60e82a6cc...06c1ae10762d3b9c1644e7fe69596ae519e015a2) --- updated-dependencies: - dependency-name: helm/kind-action dependency-version: 1.15.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/e2e-master.yaml | 2 +- .github/workflows/e2e-release-35.0.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-master.yaml b/.github/workflows/e2e-master.yaml index 4dcc5eeab8..588d369cd0 100644 --- a/.github/workflows/e2e-master.yaml +++ b/.github/workflows/e2e-master.yaml @@ -19,7 +19,7 @@ jobs: with: submodules: true - name: Create Kind Cluster - uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc + uses: helm/kind-action@06c1ae10762d3b9c1644e7fe69596ae519e015a2 with: cluster_name: kubernetes-python-e2e-master-${{ matrix.python-version }} # The kind version to be used to spin the cluster up diff --git a/.github/workflows/e2e-release-35.0.yaml b/.github/workflows/e2e-release-35.0.yaml index 7bd163e608..730710b2f2 100644 --- a/.github/workflows/e2e-release-35.0.yaml +++ b/.github/workflows/e2e-release-35.0.yaml @@ -19,7 +19,7 @@ jobs: with: submodules: true - name: Create Kind Cluster - uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc + uses: helm/kind-action@06c1ae10762d3b9c1644e7fe69596ae519e015a2 with: cluster_name: kubernetes-python-e2e-release-35.0-${{ matrix.python-version }} # The kind version to be used to spin the cluster up From a396ce1df78f331890c1c51432c81a324fe08dc9 Mon Sep 17 00:00:00 2001 From: chala2001 Date: Tue, 1 Sep 2026 17:58:18 +0530 Subject: [PATCH 03/14] Support sendInitialEvents in the dynamic client watch watch() had no way to ask the API server to replay the current state before streaming changes. Add send_initial_events and resource_version_match to it, and build both query params in request(), which was dropping them. --- kubernetes/dynamic/client.py | 12 +++++- kubernetes/dynamic/client_test.py | 71 +++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/kubernetes/dynamic/client.py b/kubernetes/dynamic/client.py index 6e62a96c3a..05f6bdb81c 100644 --- a/kubernetes/dynamic/client.py +++ b/kubernetes/dynamic/client.py @@ -158,7 +158,7 @@ def server_side_apply(self, resource, body=None, name=None, namespace=None, forc return self.request('patch', path, body=body, force_conflicts=force_conflicts, **kwargs) - def watch(self, resource, namespace=None, name=None, label_selector=None, field_selector=None, resource_version=None, timeout=None, watcher=None, allow_watch_bookmarks=None): + def watch(self, resource, namespace=None, name=None, label_selector=None, field_selector=None, resource_version=None, timeout=None, watcher=None, allow_watch_bookmarks=None, send_initial_events=None, resource_version_match=None): """ Stream events for a resource from the Kubernetes API @@ -172,6 +172,10 @@ def watch(self, resource, namespace=None, name=None, label_selector=None, field_ :param timeout: The amount of time in seconds to wait before terminating the stream :param watcher: The Watcher object that will be used to stream the resource :param allow_watch_bookmarks: Ask the API server to send BOOKMARK events + :param send_initial_events: Ask the API server to begin the stream with synthetic events + for the current state, followed by a BOOKMARK event + :param resource_version_match: How resource_version is matched, e.g. "NotOlderThan". + Required by the API server when send_initial_events is set :return: Event object with these keys: 'type': The type of event such as "ADDED", "DELETED", etc. @@ -204,6 +208,8 @@ def watch(self, resource, namespace=None, name=None, label_selector=None, field_ serialize=False, timeout_seconds=timeout, allow_watch_bookmarks=allow_watch_bookmarks, + send_initial_events=send_initial_events, + resource_version_match=resource_version_match, ): event['object'] = ResourceInstance(resource, event['object']) yield event @@ -229,6 +235,8 @@ def request(self, method, path, body=None, **params): query_params.append(('limit', params['limit'])) if params.get('resource_version') is not None: query_params.append(('resourceVersion', params['resource_version'])) + if params.get('resource_version_match') is not None: + query_params.append(('resourceVersionMatch', params['resource_version_match'])) if params.get('timeout_seconds') is not None: query_params.append(('timeoutSeconds', params['timeout_seconds'])) if params.get('watch') is not None: @@ -247,6 +255,8 @@ def request(self, method, path, body=None, **params): query_params.append(('force', params['force_conflicts'])) if params.get('allow_watch_bookmarks') is not None: query_params.append(('allowWatchBookmarks', params['allow_watch_bookmarks'])) + if params.get('send_initial_events') is not None: + query_params.append(('sendInitialEvents', params['send_initial_events'])) header_params = params.get('header_params', {}) form_params = [] diff --git a/kubernetes/dynamic/client_test.py b/kubernetes/dynamic/client_test.py index a67a2e2e3f..b424b78e9f 100644 --- a/kubernetes/dynamic/client_test.py +++ b/kubernetes/dynamic/client_test.py @@ -208,6 +208,77 @@ def log_message(self, format, *args): target.server_close() proxy.server_close() + def test_watch_forwards_send_initial_events(self): + class FakeWatcher: + def __init__(self): + self.kwargs = None + + def stream(self, func, **kwargs): + self.kwargs = kwargs + return iter(()) + + class FakeResource: + def get(self, **kwargs): + pass + + dynamic = DynamicClient.__new__(DynamicClient) + watcher = FakeWatcher() + + list(dynamic.watch( + FakeResource(), + namespace='default', + watcher=watcher, + send_initial_events=True, + resource_version_match='NotOlderThan', + )) + + self.assertEqual(True, watcher.kwargs['send_initial_events']) + self.assertEqual('NotOlderThan', watcher.kwargs['resource_version_match']) + + def test_request_builds_send_initial_events_query_params(self): + class Handler(BaseHTTPRequestHandler): + def do_GET(self): + self.server.request_path = self.path + body = json.dumps({'kind': 'APIResourceList'}).encode() + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.send_header('Content-Length', str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, format, *args): + pass + + server = ThreadingHTTPServer(('127.0.0.1', 0), Handler) + thread = threading.Thread(target=server.serve_forever) + thread.start() + try: + dynamic = DynamicClient.__new__(DynamicClient) + dynamic.client = ApiClient(Configuration( + host=f'http://127.0.0.1:{server.server_port}', + proxy='', + no_proxy='', + )) + + dynamic.request( + 'get', + '/apis', + resource_version='0', + resource_version_match='NotOlderThan', + send_initial_events=True, + serializer=lambda _, data: data, + ) + + self.assertEqual( + '/apis?resourceVersion=0&resourceVersionMatch=NotOlderThan' + '&sendInitialEvents=true', + server.request_path, + ) + finally: + server.shutdown() + thread.join() + server.server_close() + if __name__ == '__main__': unittest.main() From 863f5a89ca3fd44d0efefa4b95ca4fc5dc4f56e2 Mon Sep 17 00:00:00 2001 From: NK <92711184+nkbeast@users.noreply.github.com> Date: Fri, 4 Sep 2026 01:09:02 +0530 Subject: [PATCH 04/14] fix(leaderelection): survive malformed API error bodies and lock annotations try_acquire_or_renew() parsed the raw error body with json.loads and indexing straight into it, on the assumption that whatever came back is a Kubernetes Status object. Anything sitting in front of the API server (in an ingress, load balancer or proxy) happily answers with an HTML error page, an empty payload or some other non-JSON body, and ApiException.body can also be None. Each of those raised out of the election loop and took the whole leader election down, which is the one failure mode this code exists to prevent - a controller that stops renewing its lease without ever calling onstopped_leading leaves the workload in limbo until an operator notices. Treat an unparsable or missing error body as 'not a 404' and retry on the next period, in both the sync and the aio elector (the aio one also crashed on an empty body through an assert). The same class of problem existed on the read path of the ConfigMap lock: a corrupted leader-election annotation raised out of get() and killed the elector. Treat a non-JSON annotation like a missing one so the next update rewrites a clean record. Signed-off-by: NK Signed-off-by: NK <92711184+nkbeast@users.noreply.github.com> --- .../aio/leaderelection/leaderelection.py | 17 ++++- .../aio/leaderelection/leaderelection_test.py | 33 +++++++++ .../resourcelock/configmaplock.py | 23 +++++- kubernetes/leaderelection/leaderelection.py | 13 +++- .../leaderelection/leaderelection_test.py | 71 +++++++++++++++++++ .../resourcelock/configmaplock.py | 15 +++- 6 files changed, 163 insertions(+), 9 deletions(-) diff --git a/kubernetes/aio/leaderelection/leaderelection.py b/kubernetes/aio/leaderelection/leaderelection.py index 1289692b92..fb8938bd6f 100644 --- a/kubernetes/aio/leaderelection/leaderelection.py +++ b/kubernetes/aio/leaderelection/leaderelection.py @@ -145,11 +145,22 @@ async def try_acquire_or_renew(self) -> bool: # A lock is not created with that name, try to create one if not lock_status: - assert ( + # The error body comes straight from the API server, but anything + # sitting in front of it (ingress, load balancer, proxy) can answer + # with an HTML page, an empty payload or some other non-JSON body. + # Only a clean 404 means the lock is absent and may be created; + # everything else is retried on the next period instead of taking + # the whole leader election down. + error_code = None + if ( isinstance(old_election_record, ApiException) and old_election_record.body is not None - ) - if json.loads(old_election_record.body)["code"] != HTTPStatus.NOT_FOUND: + ): + try: + error_code = json.loads(old_election_record.body)["code"] + except (ValueError, TypeError, KeyError, AttributeError): + error_code = None + if error_code != HTTPStatus.NOT_FOUND: logger.error( "Error retrieving resource lock %s as %s", self.election_config.lock.name, diff --git a/kubernetes/aio/leaderelection/leaderelection_test.py b/kubernetes/aio/leaderelection/leaderelection_test.py index 3d1e35dc0e..c1e3656227 100644 --- a/kubernetes/aio/leaderelection/leaderelection_test.py +++ b/kubernetes/aio/leaderelection/leaderelection_test.py @@ -365,5 +365,38 @@ async def update( self.lock.release() + def test_acquire_survives_non_json_error_body(self): + """A proxy answering with an HTML error page must not kill the elector.""" + + class GatewayErrorLock: + def __init__(self): + self.name = "lock" + self.namespace = "ns" + self.identity = "candidate" + + async def get(self, name, namespace): + return False, ApiException( + status=502, + reason="Bad Gateway", + body="502 Bad Gateway", + ) + + async def create(self, name, namespace, election_record): + return False + + config = electionconfig.Config( + lock=GatewayErrorLock(), + lease_duration=4, + renew_deadline=3, + retry_period=1, + onstarted_leading=lambda: None, + onstopped_leading=lambda: None, + ) + + elector = leaderelection.LeaderElection(config) + result = asyncio.run(elector.try_acquire_or_renew()) + self.assertFalse(result) + + if __name__ == "__main__": unittest.main() diff --git a/kubernetes/aio/leaderelection/resourcelock/configmaplock.py b/kubernetes/aio/leaderelection/resourcelock/configmaplock.py index 53a46c09bf..aa4c4862da 100644 --- a/kubernetes/aio/leaderelection/resourcelock/configmaplock.py +++ b/kubernetes/aio/leaderelection/resourcelock/configmaplock.py @@ -80,9 +80,26 @@ async def get( self.configmap_reference = api_response return True, None - lock_record = self.get_lock_object( - json.loads(annotations[self.leader_electionrecord_annotationkey]) - ) + # A corrupted annotation must not take the elector down: treat it + # like a missing one so the next update rewrites a clean record. + try: + annotation_record = json.loads( + annotations[self.leader_electionrecord_annotationkey] + ) + except ValueError: + logger.warning( + "Leader election annotation on ConfigMap %s/%s is not valid " + "JSON; treating the lock as unheld", + name, + namespace, + ) + api_response.metadata.annotations = { + self.leader_electionrecord_annotationkey: "" + } + self.configmap_reference = api_response + return True, None + + lock_record = self.get_lock_object(annotation_record) self.configmap_reference = api_response return True, lock_record diff --git a/kubernetes/leaderelection/leaderelection.py b/kubernetes/leaderelection/leaderelection.py index fc72a1d95e..951511f513 100644 --- a/kubernetes/leaderelection/leaderelection.py +++ b/kubernetes/leaderelection/leaderelection.py @@ -130,8 +130,17 @@ def try_acquire_or_renew(self): # A lock is not created with that name, try to create one if not lock_status: - if json.loads(old_election_record.body)[ - 'code'] != HTTPStatus.NOT_FOUND: + # The error body comes straight from the API server, but anything + # sitting in front of it (ingress, load balancer, proxy) can answer + # with an HTML page, an empty payload or some other non-JSON body. + # Only a clean 404 means the lock is absent and may be created; + # everything else is retried on the next period instead of taking + # the whole leader election down. + try: + error_code = json.loads(old_election_record.body)['code'] + except (ValueError, TypeError, KeyError, AttributeError): + error_code = None + if error_code != HTTPStatus.NOT_FOUND: logger.info( "Error retrieving resource lock {} as {}".format( self.election_config.lock.name, diff --git a/kubernetes/leaderelection/leaderelection_test.py b/kubernetes/leaderelection/leaderelection_test.py index ad9c7e7d1e..ba12b49222 100644 --- a/kubernetes/leaderelection/leaderelection_test.py +++ b/kubernetes/leaderelection/leaderelection_test.py @@ -321,5 +321,76 @@ def update(self, name, namespace, updated_record): self.lock.release() + def test_acquire_survives_non_json_error_body(self): + """A proxy answering with an HTML error page must not kill the elector.""" + class GatewayErrorLock: + def __init__(self): + self.name = "lock" + self.namespace = "ns" + self.identity = "candidate" + + def get(self, name, namespace): + return False, ApiException( + status=502, reason="Bad Gateway", + body="502 Bad Gateway") + + config = electionconfig.Config( + lock=GatewayErrorLock(), lease_duration=4, renew_deadline=3, + retry_period=1, onstarted_leading=lambda: None, + onstopped_leading=lambda: None) + + result = leaderelection.LeaderElection(config).try_acquire_or_renew() + self.assertFalse(result) + + def test_acquire_survives_empty_error_body(self): + class EmptyErrorLock: + def __init__(self): + self.name = "lock" + self.namespace = "ns" + self.identity = "candidate" + + def get(self, name, namespace): + return False, ApiException(status=500, reason="Server Error", + body=None) + + config = electionconfig.Config( + lock=EmptyErrorLock(), lease_duration=4, renew_deadline=3, + retry_period=1, onstarted_leading=lambda: None, + onstopped_leading=lambda: None) + + result = leaderelection.LeaderElection(config).try_acquire_or_renew() + self.assertFalse(result) + + def test_acquire_still_creates_on_clean_404(self): + class NotFoundLock: + def __init__(self): + self.name = "lock" + self.namespace = "ns" + self.identity = "candidate" + self.created = False + + def get(self, name, namespace): + if self.created: + return True, LeaderElectionRecord( + "candidate", "4", "now", "now") + return False, ApiException( + status=404, reason="Not Found", + body=json.dumps({'code': 404})) + + def create(self, name, namespace, election_record): + self.created = True + return True + + def update(self, name, namespace, updated_record): + return True + + config = electionconfig.Config( + lock=NotFoundLock(), lease_duration=4, renew_deadline=3, + retry_period=1, onstarted_leading=lambda: None, + onstopped_leading=lambda: None) + + self.assertTrue(leaderelection.LeaderElection(config).try_acquire_or_renew()) + + if __name__ == '__main__': unittest.main() diff --git a/kubernetes/leaderelection/resourcelock/configmaplock.py b/kubernetes/leaderelection/resourcelock/configmaplock.py index c2f1e1cc67..bb694a9b93 100644 --- a/kubernetes/leaderelection/resourcelock/configmaplock.py +++ b/kubernetes/leaderelection/resourcelock/configmaplock.py @@ -64,7 +64,20 @@ def get(self, name, namespace): self.configmap_reference = api_response return True, None - lock_record = self.get_lock_object(json.loads(annotations[self.leader_electionrecord_annotationkey])) + # A corrupted annotation must not take the elector down: treat it + # like a missing one so the next update rewrites a clean record. + try: + annotation_record = json.loads( + annotations[self.leader_electionrecord_annotationkey]) + except ValueError: + logger.warning( + "Leader election annotation on ConfigMap {}/{} is not valid " + "JSON; treating the lock as unheld".format(name, namespace)) + api_response.metadata.annotations = {self.leader_electionrecord_annotationkey: ''} + self.configmap_reference = api_response + return True, None + + lock_record = self.get_lock_object(annotation_record) self.configmap_reference = api_response return True, lock_record From 43b3c88098629b99cbc32173b765dcb22e97ea2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 02:02:54 +0000 Subject: [PATCH 05/14] Update coverage requirement from >=7.16.0 to >=7.16.1 Updates the requirements on [coverage](https://github.com/coveragepy/coveragepy) to permit the latest version. - [Release notes](https://github.com/coveragepy/coveragepy/releases) - [Changelog](https://github.com/coveragepy/coveragepy/blob/main/CHANGES.rst) - [Commits](https://github.com/coveragepy/coveragepy/compare/7.16.0...7.16.1) --- updated-dependencies: - dependency-name: coverage dependency-version: 7.16.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index d3c93b1f73..ca23638022 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,4 @@ -coverage>=7.16.0 +coverage>=7.16.1 nose>=1.3.7 pytest pytest-cov From 09903f8c7d6656f122023159cf2ca84f7e056487 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:02:21 +0000 Subject: [PATCH 06/14] Bump codecov/codecov-action from 7.0.0 to 7.1.0 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 7.0.0 to 7.1.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/fb8b3582c8e4def4969c97caa2f19720cb33a72f...0b35c9ecc4f0529d0eb674914510c22f85b196b4) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f133c182e9..f86b8a12e0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -45,7 +45,7 @@ jobs: - name: Upload coverage to Codecov if: "matrix.use_coverage" - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f + uses: codecov/codecov-action@0b35c9ecc4f0529d0eb674914510c22f85b196b4 with: fail_ci_if_error: false verbose: true From da7c08ab25e2b37fa47969309f61fb12ff7d845a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 02:02:51 +0000 Subject: [PATCH 07/14] Update urllib3 requirement from >=2.7.0 to >=2.8.0 Updates the requirements on [urllib3](https://github.com/urllib3/urllib3) to permit the latest version. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.7.0...2.8.0) --- updated-dependencies: - dependency-name: urllib3 dependency-version: 2.8.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements-asyncio.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-asyncio.txt b/requirements-asyncio.txt index 245285704c..75a435fdf5 100644 --- a/requirements-asyncio.txt +++ b/requirements-asyncio.txt @@ -1,6 +1,6 @@ python-dateutil>=2.8.2 # BSD PyYAML>=6.0.3 # MIT -urllib3>=2.7.0 # MIT +urllib3>=2.8.0 # MIT aiohttp>=3.14.3,<4.0.0 # Apache-2.0 aiohttp-retry>=2.9.1 # MIT pydantic>=2.13.5 # MIT diff --git a/requirements.txt b/requirements.txt index 5b4d4f465b..b30649eed7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,5 +7,5 @@ websocket-client>=0.32.0,!=0.40.0,!=0.41.*,!=0.42.* # LGPLv2+ requests # Apache-2.0 requests-oauthlib # ISC typing-extensions>=4.16.0 # PSF -urllib3>=2.7.0,<3.0.0 # MIT +urllib3>=2.8.0,<3.0.0 # MIT durationpy>=0.11 # MIT From 8b80a5f80351d545002bc7b543e9148b805a80c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 03:02:29 +0000 Subject: [PATCH 08/14] Bump codecov/codecov-action from 7.1.0 to 7.1.1 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 7.1.0 to 7.1.1. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/0b35c9ecc4f0529d0eb674914510c22f85b196b4...303a32d7a59b442fa8d48b6a1cc6825c09c847a5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: 7.1.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f86b8a12e0..692d488ff8 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -45,7 +45,7 @@ jobs: - name: Upload coverage to Codecov if: "matrix.use_coverage" - uses: codecov/codecov-action@0b35c9ecc4f0529d0eb674914510c22f85b196b4 + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 with: fail_ci_if_error: false verbose: true From 1544af4d8e95af9d50c1eb2aaa0189ef8e92a252 Mon Sep 17 00:00:00 2001 From: yliao Date: Tue, 8 Sep 2026 00:00:49 +0000 Subject: [PATCH 09/14] added dynamic, leaderelection, stream, watch to setup-release.py --- kubernetes/aio/__init__.py | 3 ++- setup-release.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/kubernetes/aio/__init__.py b/kubernetes/aio/__init__.py index c1415fbed9..9741e9e0bf 100644 --- a/kubernetes/aio/__init__.py +++ b/kubernetes/aio/__init__.py @@ -19,8 +19,9 @@ import kubernetes.aio.client as client import kubernetes.aio.config as config import kubernetes.aio.dynamic as dynamic +import kubernetes.aio.leaderelection as leaderelection import kubernetes.aio.stream as stream import kubernetes.aio.utils as utils import kubernetes.aio.watch as watch -__all__ = ["client", "config", "dynamic", "stream", "utils", "watch"] +__all__ = ["client", "config", "dynamic", "leaderelection", "stream", "utils", "watch"] diff --git a/setup-release.py b/setup-release.py index 9ebf622c4c..136ff8b5e9 100644 --- a/setup-release.py +++ b/setup-release.py @@ -74,11 +74,16 @@ 'kubernetes.leaderelection.resourcelock', 'kubernetes.informer', 'kubernetes.aio', - 'kubernetes.aio.config', - 'kubernetes.aio.utils', 'kubernetes.aio.client', 'kubernetes.aio.client.api', - 'kubernetes.aio.client.models' + 'kubernetes.aio.client.models', + 'kubernetes.aio.config', + 'kubernetes.aio.dynamic', + 'kubernetes.aio.leaderelection', + 'kubernetes.aio.leaderelection.resourcelock', + 'kubernetes.aio.stream', + 'kubernetes.aio.utils', + 'kubernetes.aio.watch', ], package_data={ 'kubernetes': ['py.typed'], From 85ee9ffbb65bca78fa16881bf219a36e201acf23 Mon Sep 17 00:00:00 2001 From: yliao Date: Tue, 22 Sep 2026 14:49:17 -0700 Subject: [PATCH 10/14] update version constants for 37.0.0b1 release --- scripts/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/constants.py b/scripts/constants.py index ab62395509..9fcd48f885 100644 --- a/scripts/constants.py +++ b/scripts/constants.py @@ -18,13 +18,13 @@ KUBERNETES_BRANCH = "release-1.37" # client version for packaging and releasing. -CLIENT_VERSION = "37.0.0a1" +CLIENT_VERSION = "37.0.0b1" # Name of the release package PACKAGE_NAME = "kubernetes" # Stage of development, mainly used in setup.py's classifiers. -DEVELOPMENT_STATUS = "3 - Alpha" +DEVELOPMENT_STATUS = "4 - Beta" # If called directly, return the constant value given From c7b046d37aacf2dae79ac3b83a01e16b0c25eafb Mon Sep 17 00:00:00 2001 From: yliao Date: Tue, 22 Sep 2026 14:49:18 -0700 Subject: [PATCH 11/14] update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25be22e4f2..b12502e080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# v37.0.0b1 + +Kubernetes API Version: v1.37.0 + + # v37.0.0a1 Kubernetes API Version: v1.37.0 From 4a8f56b58bbc72c4281500af5d4e89bf018b72d2 Mon Sep 17 00:00:00 2001 From: yliao Date: Tue, 22 Sep 2026 14:50:49 -0700 Subject: [PATCH 12/14] generated API change --- doc/source/kubernetes.utils.keepalive.rst | 7 +++++++ doc/source/kubernetes.utils.keepalive_test.rst | 7 +++++++ doc/source/kubernetes.utils.rst | 2 ++ 3 files changed, 16 insertions(+) create mode 100644 doc/source/kubernetes.utils.keepalive.rst create mode 100644 doc/source/kubernetes.utils.keepalive_test.rst diff --git a/doc/source/kubernetes.utils.keepalive.rst b/doc/source/kubernetes.utils.keepalive.rst new file mode 100644 index 0000000000..b00e7d84f7 --- /dev/null +++ b/doc/source/kubernetes.utils.keepalive.rst @@ -0,0 +1,7 @@ +kubernetes.utils.keepalive module +================================= + +.. automodule:: kubernetes.utils.keepalive + :members: + :show-inheritance: + :undoc-members: diff --git a/doc/source/kubernetes.utils.keepalive_test.rst b/doc/source/kubernetes.utils.keepalive_test.rst new file mode 100644 index 0000000000..58fe418ede --- /dev/null +++ b/doc/source/kubernetes.utils.keepalive_test.rst @@ -0,0 +1,7 @@ +kubernetes.utils.keepalive\_test module +======================================= + +.. automodule:: kubernetes.utils.keepalive_test + :members: + :show-inheritance: + :undoc-members: diff --git a/doc/source/kubernetes.utils.rst b/doc/source/kubernetes.utils.rst index ffcb94d255..ea24ab079c 100644 --- a/doc/source/kubernetes.utils.rst +++ b/doc/source/kubernetes.utils.rst @@ -9,6 +9,8 @@ Submodules kubernetes.utils.create_from_yaml kubernetes.utils.duration + kubernetes.utils.keepalive + kubernetes.utils.keepalive_test kubernetes.utils.metrics kubernetes.utils.quantity kubernetes.utils.retry From aeb765c21cc9146f1e11460c42cf395ff77efaf5 Mon Sep 17 00:00:00 2001 From: yliao Date: Tue, 22 Sep 2026 14:50:50 -0700 Subject: [PATCH 13/14] generated client change --- kubernetes/README.md | 2 +- kubernetes/__init__.py | 2 +- kubernetes/aio/README.md | 2 +- kubernetes/aio/__init__.py | 2 +- kubernetes/aio/client/__init__.py | 2 +- kubernetes/aio/client/api_client.py | 2 +- kubernetes/aio/client/configuration.py | 2 +- kubernetes/client/__init__.py | 2 +- kubernetes/client/api_client.py | 2 +- kubernetes/client/configuration.py | 2 +- setup-asyncio.py | 4 ++-- setup-release.py | 4 ++-- setup.py | 4 ++-- 13 files changed, 16 insertions(+), 16 deletions(-) diff --git a/kubernetes/README.md b/kubernetes/README.md index 2f9e603a68..7254a43d69 100644 --- a/kubernetes/README.md +++ b/kubernetes/README.md @@ -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.37 -- Package version: 37.0.0a1 +- Package version: 37.0.0b1 - Generator version: 7.25.0 - Build package: org.openapitools.codegen.languages.PythonClientCodegen diff --git a/kubernetes/__init__.py b/kubernetes/__init__.py index 85f1252ce3..18d57b9faf 100644 --- a/kubernetes/__init__.py +++ b/kubernetes/__init__.py @@ -14,7 +14,7 @@ __project__ = 'kubernetes' # The version is auto-updated. Please do not edit. -__version__ = "37.0.0a1" +__version__ = "37.0.0b1" from . import client from . import config diff --git a/kubernetes/aio/README.md b/kubernetes/aio/README.md index 4ff138f178..fdf8e069d8 100644 --- a/kubernetes/aio/README.md +++ b/kubernetes/aio/README.md @@ -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.37 -- Package version: 37.0.0a1 +- Package version: 37.0.0b1 - Generator version: 7.25.0 - Build package: org.openapitools.codegen.languages.PythonClientCodegen diff --git a/kubernetes/aio/__init__.py b/kubernetes/aio/__init__.py index 9741e9e0bf..702c62c04d 100644 --- a/kubernetes/aio/__init__.py +++ b/kubernetes/aio/__init__.py @@ -14,7 +14,7 @@ __project__ = "kubernetes_aio" # The version is auto-updated. Please do not edit. -__version__ = "37.0.0a1" +__version__ = "37.0.0b1" import kubernetes.aio.client as client import kubernetes.aio.config as config diff --git a/kubernetes/aio/client/__init__.py b/kubernetes/aio/client/__init__.py index f48967fd11..14ba8c3e83 100644 --- a/kubernetes/aio/client/__init__.py +++ b/kubernetes/aio/client/__init__.py @@ -14,7 +14,7 @@ """ # noqa: E501 -__version__ = "37.0.0a1" +__version__ = "37.0.0b1" # Define package exports __all__ = [ diff --git a/kubernetes/aio/client/api_client.py b/kubernetes/aio/client/api_client.py index 73b5e241b9..80b3db2803 100644 --- a/kubernetes/aio/client/api_client.py +++ b/kubernetes/aio/client/api_client.py @@ -122,7 +122,7 @@ def __init__( self.default_headers[header_name] = header_value self.cookie = cookie # Set default User-Agent. - self.user_agent = 'OpenAPI-Generator/37.0.0a1/python' + self.user_agent = 'OpenAPI-Generator/37.0.0b1/python' self.client_side_validation = configuration.client_side_validation async def __aenter__(self): diff --git a/kubernetes/aio/client/configuration.py b/kubernetes/aio/client/configuration.py index 06f0ce3212..6562e03619 100644 --- a/kubernetes/aio/client/configuration.py +++ b/kubernetes/aio/client/configuration.py @@ -612,7 +612,7 @@ def to_debug_report(self) -> str: "OS: {env}\n"\ "Python Version: {pyversion}\n"\ "Version of the API: release-1.37\n"\ - "SDK Package Version: 37.0.0a1".\ + "SDK Package Version: 37.0.0b1".\ format(env=sys.platform, pyversion=sys.version) def get_host_settings(self) -> List[HostSetting]: diff --git a/kubernetes/client/__init__.py b/kubernetes/client/__init__.py index e5464b24c9..96bbeb31bc 100644 --- a/kubernetes/client/__init__.py +++ b/kubernetes/client/__init__.py @@ -14,7 +14,7 @@ """ # noqa: E501 -__version__ = "37.0.0a1" +__version__ = "37.0.0b1" # Define package exports __all__ = [ diff --git a/kubernetes/client/api_client.py b/kubernetes/client/api_client.py index 2c0a5e1336..77a75b9f6e 100644 --- a/kubernetes/client/api_client.py +++ b/kubernetes/client/api_client.py @@ -129,7 +129,7 @@ def __init__( self.pool_threads = pool_threads self._pool_lock = Lock() # Set default User-Agent. - self.user_agent = 'OpenAPI-Generator/37.0.0a1/python' + self.user_agent = 'OpenAPI-Generator/37.0.0b1/python' self.client_side_validation = configuration.client_side_validation def close(self): diff --git a/kubernetes/client/configuration.py b/kubernetes/client/configuration.py index 4caa1af87b..731b382440 100644 --- a/kubernetes/client/configuration.py +++ b/kubernetes/client/configuration.py @@ -613,7 +613,7 @@ def to_debug_report(self) -> str: "OS: {env}\n"\ "Python Version: {pyversion}\n"\ "Version of the API: release-1.37\n"\ - "SDK Package Version: 37.0.0a1".\ + "SDK Package Version: 37.0.0b1".\ format(env=sys.platform, pyversion=sys.version) def get_host_settings(self) -> List[HostSetting]: diff --git a/setup-asyncio.py b/setup-asyncio.py index 2e221af18f..a5de317346 100644 --- a/setup-asyncio.py +++ b/setup-asyncio.py @@ -16,9 +16,9 @@ # Do not edit these constants. They will be updated automatically # by scripts/update-client-asyncio.sh. -CLIENT_VERSION = "37.0.0a1" +CLIENT_VERSION = "37.0.0b1" PACKAGE_NAME = "kubernetes.aio" -DEVELOPMENT_STATUS = "3 - Alpha" +DEVELOPMENT_STATUS = "4 - Beta" # To install the library, run the following # diff --git a/setup-release.py b/setup-release.py index 136ff8b5e9..8050d218f9 100644 --- a/setup-release.py +++ b/setup-release.py @@ -16,9 +16,9 @@ # Do not edit these constants. They will be updated automatically # by scripts/update-client.sh. -CLIENT_VERSION = "37.0.0a1" +CLIENT_VERSION = "37.0.0b1" PACKAGE_NAME = "kubernetes" -DEVELOPMENT_STATUS = "3 - Alpha" +DEVELOPMENT_STATUS = "4 - Beta" # To install the library, run the following # diff --git a/setup.py b/setup.py index edf7244c47..6561541574 100644 --- a/setup.py +++ b/setup.py @@ -16,9 +16,9 @@ # Do not edit these constants. They will be updated automatically # by scripts/update-client.sh. -CLIENT_VERSION = "37.0.0a1" +CLIENT_VERSION = "37.0.0b1" PACKAGE_NAME = "kubernetes" -DEVELOPMENT_STATUS = "3 - Alpha" +DEVELOPMENT_STATUS = "4 - Beta" # To install the library, run the following # From 3715c8b75584960b68efa078868a6fa49597662e Mon Sep 17 00:00:00 2001 From: yliao Date: Tue, 22 Sep 2026 14:55:36 -0700 Subject: [PATCH 14/14] Update the compatibility matrix and maintenance status --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e4b71d0a6c..03f0988cd6 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ supported versions of Kubernetes clusters. - [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.3/): Kubernetes 1.35 or below (+-), Kubernetes 1.36 (✓), Kubernetes 1.37 or above (+-) -- [client 37.y.z](https://pypi.org/project/kubernetes/37.0.0/): Kubernetes 1.36 or below (+-), Kubernetes 1.37 (✓), Kubernetes 1.38 or above (+-) +- [client 37.y.z](https://pypi.org/project/kubernetes/37.0.0b1/): Kubernetes 1.36 or below (+-), Kubernetes 1.37 (✓), Kubernetes 1.38 or above (+-) > See [here](#homogenizing-the-kubernetes-python-client-versions) for an explanation of why there is no v13-v16 release.