Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/e2e-master.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:

- name: Upload coverage to Codecov
if: "matrix.use_coverage"
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5
with:
fail_ci_if_error: false
verbose: true
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# v37.0.0b1

Kubernetes API Version: v1.37.0


# v37.0.0a1

Kubernetes API Version: v1.37.0
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions doc/source/kubernetes.utils.keepalive.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
kubernetes.utils.keepalive module
=================================

.. automodule:: kubernetes.utils.keepalive
:members:
:show-inheritance:
:undoc-members:
7 changes: 7 additions & 0 deletions doc/source/kubernetes.utils.keepalive_test.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
kubernetes.utils.keepalive\_test module
=======================================

.. automodule:: kubernetes.utils.keepalive_test
:members:
:show-inheritance:
:undoc-members:
2 changes: 2 additions & 0 deletions doc/source/kubernetes.utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
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.37
- Package version: 37.0.0a1
- Package version: 37.0.0b1
- Generator version: 7.25.0
- Build package: org.openapitools.codegen.languages.PythonClientCodegen

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__ = "37.0.0a1"
__version__ = "37.0.0b1"

from . import client
from . import config
Expand Down
2 changes: 1 addition & 1 deletion kubernetes/aio/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.37
- Package version: 37.0.0a1
- Package version: 37.0.0b1
- Generator version: 7.25.0
- Build package: org.openapitools.codegen.languages.PythonClientCodegen

Expand Down
5 changes: 3 additions & 2 deletions kubernetes/aio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@

__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
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"]
2 changes: 1 addition & 1 deletion kubernetes/aio/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
""" # noqa: E501


__version__ = "37.0.0a1"
__version__ = "37.0.0b1"

# Define package exports
__all__ = [
Expand Down
2 changes: 1 addition & 1 deletion kubernetes/aio/client/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion kubernetes/aio/client/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
17 changes: 14 additions & 3 deletions kubernetes/aio/leaderelection/leaderelection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
33 changes: 33 additions & 0 deletions kubernetes/aio/leaderelection/leaderelection_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="<html><body>502 Bad Gateway</body></html>",
)

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()
23 changes: 20 additions & 3 deletions kubernetes/aio/leaderelection/resourcelock/configmaplock.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion kubernetes/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
""" # noqa: E501


__version__ = "37.0.0a1"
__version__ = "37.0.0b1"

# Define package exports
__all__ = [
Expand Down
2 changes: 1 addition & 1 deletion kubernetes/client/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
11 changes: 10 additions & 1 deletion kubernetes/client/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -604,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]:
Expand Down
3 changes: 3 additions & 0 deletions kubernetes/client/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion kubernetes/dynamic/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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 = []
Expand Down
Loading
Loading