Skip to content
Open
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/deploy-wiki.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
deploy-wiki:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
submodules: true
- name: Install rsync
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/e2e-master.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
submodules: true
- name: Create Kind Cluster
Expand All @@ -30,7 +30,7 @@ jobs:
# as we sync with Kubernetes upstream
config: .github/workflows/kind-configs/cluster-1.35.yaml
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/e2e-release-35.0.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
matrix:
python-version: [3.10, 3.11, 3.12]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
submodules: true
- name: Create Kind Cluster
Expand All @@ -30,7 +30,7 @@ jobs:
# as we sync with Kubernetes upstream
config: .github/workflows/kind-configs/cluster-1.35.yaml
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ jobs:
use_coverage: 'coverage'

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
submodules: true
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand Down Expand Up @@ -45,7 +45,7 @@ jobs:

- name: Upload coverage to Codecov
if: "matrix.use_coverage"
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f
with:
fail_ci_if_error: false
verbose: true
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# v36.0.3

Kubernetes API Version: v1.36.2

### Bug or Regression
- Fix Watch.stream selecting watch instead of follow when streaming pod logs.
- Start the leader election worker thread as a daemon so it does not block process shutdown.
- Fix readline_channel, readline_stdout and readline_stderr crashing with OverflowError when using the default timeout, and returning None when a timeout expires.
- Fix format_quantity returning imprecise, non-canonical values for the milli, micro and nano suffixes, and ignoring quantize=Decimal(0).

# v36.0.2

Kubernetes API Version: v1.36.1
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.2
- Package version: 36.0.3
- 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.2"
__version__ = "36.0.3"

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.36
- Package version: 36.0.2
- Package version: 36.0.3
- Build package: org.openapitools.codegen.languages.PythonLegacyClientCodegen

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

__project__ = "kubernetes_aio"
# The version is auto-updated. Please do not edit.
__version__ = "36.0.2"
__version__ = "36.0.3"

import kubernetes.aio.client as client

Expand Down
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 @@

from __future__ import absolute_import

__version__ = "36.0.2"
__version__ = "36.0.3"

# import apis into sdk package
from kubernetes.aio.client.api.well_known_api import WellKnownApi
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 @@ -78,7 +78,7 @@ def __init__(self, configuration=None, header_name=None, header_value=None,
self.default_headers[header_name] = header_value
self.cookie = cookie
# Set default User-Agent.
self.user_agent = 'OpenAPI-Generator/36.0.2/python'
self.user_agent = 'OpenAPI-Generator/36.0.3/python'
self.client_side_validation = configuration.client_side_validation

async def __aenter__(self):
Expand Down
2 changes: 1 addition & 1 deletion kubernetes/aio/client/api_client.py.orig
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class ApiClient(object):
self.default_headers[header_name] = header_value
self.cookie = cookie
# Set default User-Agent.
self.user_agent = 'OpenAPI-Generator/36.0.2/python'
self.user_agent = 'OpenAPI-Generator/36.0.3/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 @@ -441,7 +441,7 @@ def to_debug_report(self):
"OS: {env}\n"\
"Python Version: {pyversion}\n"\
"Version of the API: release-1.36\n"\
"SDK Package Version: 36.0.2".\
"SDK Package Version: 36.0.3".\
format(env=sys.platform, pyversion=sys.version)

def get_host_settings(self):
Expand Down
2 changes: 1 addition & 1 deletion kubernetes/aio/client/configuration.py.orig
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ conf = client.Configuration(
"OS: {env}\n"\
"Python Version: {pyversion}\n"\
"Version of the API: release-1.36\n"\
"SDK Package Version: 36.0.2".\
"SDK Package Version: 36.0.3".\
format(env=sys.platform, pyversion=sys.version)

def get_host_settings(self):
Expand Down
2 changes: 1 addition & 1 deletion kubernetes/aio/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from setuptools import setup, find_packages # noqa: H301

NAME = "client"
VERSION = "36.0.2"
VERSION = "36.0.3"
# To install the library, run the following
#
# python setup.py install
Expand Down
3 changes: 1 addition & 2 deletions kubernetes/base/leaderelection/leaderelection.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ def run(self):
logger.info("{} successfully acquired lease".format(self.election_config.lock.identity))

# Start leading and call OnStartedLeading()
threading.daemon = True
threading.Thread(target=self.election_config.onstarted_leading).start()
threading.Thread(target=self.election_config.onstarted_leading, daemon=True).start()

self.renew_loop()

Expand Down
28 changes: 28 additions & 0 deletions kubernetes/base/leaderelection/leaderelection_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import json
import time
import pytest
from unittest.mock import patch

thread_lock = threading.RLock()

Expand Down Expand Up @@ -194,6 +195,33 @@ def on_stopped_leading():

self.assert_history(leadership_history, ["get leadership", "start leading", "stop leading"])

def test_onstarted_leading_runs_in_daemon_thread(self):
captured = {}
real_thread = threading.Thread

def record_thread(*args, **kwargs):
thread = real_thread(*args, **kwargs)
captured["daemon"] = thread.daemon
return thread

started = threading.Event()

mock_lock = MockResourceLock("mock", "mock_namespace", "mock", thread_lock,
lambda: None, lambda: None, lambda: None, None)
mock_lock.renew_count_max = 1

config = electionconfig.Config(lock=mock_lock, lease_duration=2,
renew_deadline=1.5, retry_period=1,
onstarted_leading=started.set,
onstopped_leading=lambda: None)

with patch.object(leaderelection.threading, "Thread", new=record_thread):
leaderelection.LeaderElection(config).run()

self.assertTrue(started.wait(1), "onstarted_leading callback did not run")
self.assertIn("daemon", captured)
self.assertTrue(captured["daemon"])

def assert_history(self, history, expected):
self.assertIsNotNone(expected)
self.assertIsNotNone(history)
Expand Down
7 changes: 6 additions & 1 deletion kubernetes/base/stream/ws_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ def readline_channel(self, channel, timeout=None):
return b"" if self.binary else ""

self.update(timeout=(timeout - time.time() + start))
return b"" if self.binary else ""

def write_channel(self, channel, data):
"""Write data to a channel."""
Expand Down Expand Up @@ -216,11 +217,15 @@ def update(self, timeout=0):
if hasattr(select, "poll"):
poll = select.poll()
poll.register(self.sock.sock, select.POLLIN)
if timeout is not None:
if timeout is not None and timeout != float("inf"):
timeout *= 1_000 # poll method uses milliseconds as the time unit
else:
timeout = None
r = poll.poll(timeout)
poll.unregister(self.sock.sock)
else:
if timeout == float("inf"):
timeout = None
r, _, _ = select.select(
(self.sock.sock, ), (), (), timeout)

Expand Down
42 changes: 42 additions & 0 deletions kubernetes/base/stream/ws_client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,48 @@ def test_peek_channel_closed_with_leftover_data(self):
self.assertEqual(data3, "")
mock_update.assert_not_called()

def test_update_infinite_timeout_polls_without_overflow(self):
"""Verify update maps an infinite (default) timeout to a blocking poll instead of overflowing"""
with patch.object(ws_client_module, 'create_websocket') as mock_create, \
patch('select.poll') as mock_poll:
mock_poll.return_value.poll.return_value = []
mock_ws = MagicMock()
mock_ws.subprotocol = V5_CHANNEL_PROTOCOL
mock_ws.connected = True
mock_ws.sock.fileno.return_value = 10
mock_create.return_value = mock_ws

client = WSClient(self.config_mock, "ws://test", headers=None, capture_all=True)
client.update(timeout=float("inf"))

mock_poll.return_value.poll.assert_called_once_with(None)

def test_readline_channel_returns_empty_string_on_expired_timeout(self):
"""Verify readline_channel returns '' (not None) when a finite timeout expires"""
with patch.object(ws_client_module, 'create_websocket') as mock_create:
mock_ws = MagicMock()
mock_ws.subprotocol = V5_CHANNEL_PROTOCOL
mock_ws.connected = True
mock_create.return_value = mock_ws

client = WSClient(self.config_mock, "ws://test", headers=None, capture_all=True, binary=False)
with patch.object(client, 'update'):
line = client.readline_channel(1, timeout=0.01)
self.assertEqual(line, "")

def test_readline_channel_returns_empty_bytes_on_expired_timeout(self):
"""Verify readline_channel returns b'' (not None) when a finite timeout expires in binary mode"""
with patch.object(ws_client_module, 'create_websocket') as mock_create:
mock_ws = MagicMock()
mock_ws.subprotocol = V5_CHANNEL_PROTOCOL
mock_ws.connected = True
mock_create.return_value = mock_ws

client = WSClient(self.config_mock, "ws://test", headers=None, capture_all=True, binary=True)
with patch.object(client, 'update'):
line = client.readline_channel(1, timeout=0.01)
self.assertEqual(line, b"")



@pytest.fixture(scope="module")
Expand Down
2 changes: 1 addition & 1 deletion kubernetes/base/watch/watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from kubernetes import client

PYDOC_RETURN_LABEL = ":rtype:"
PYDOC_FOLLOW_PARAM = ":param bool follow:"
PYDOC_FOLLOW_PARAM = ":param follow:"

# Removing this suffix from return type name should give us event's object
# type. e.g., if list_namespaces() returns "NamespaceList" type,
Expand Down
3 changes: 2 additions & 1 deletion kubernetes/base/watch/watch_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,8 @@ def test_watch_for_follow(self):

fake_api = Mock()
fake_api.read_namespaced_pod_log = Mock(return_value=fake_resp)
fake_api.read_namespaced_pod_log.__doc__ = ':param bool follow:\n:rtype: str'
fake_api.read_namespaced_pod_log.__doc__ = (
':param follow:\n:type follow: bool\n:rtype: str')

w = Watch()
count = 1
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 @@

from __future__ import absolute_import

__version__ = "36.0.2"
__version__ = "36.0.3"

# import apis into sdk package
from kubernetes.client.api.well_known_api import WellKnownApi
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 @@ -78,7 +78,7 @@ def __init__(self, configuration=None, header_name=None, header_value=None,
self.default_headers[header_name] = header_value
self.cookie = cookie
# Set default User-Agent.
self.user_agent = 'OpenAPI-Generator/36.0.2/python'
self.user_agent = 'OpenAPI-Generator/36.0.3/python'
self.client_side_validation = configuration.client_side_validation

def __enter__(self):
Expand Down
2 changes: 1 addition & 1 deletion kubernetes/client/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ def to_debug_report(self):
"OS: {env}\n"\
"Python Version: {pyversion}\n"\
"Version of the API: release-1.36\n"\
"SDK Package Version: 36.0.2".\
"SDK Package Version: 36.0.3".\
format(env=sys.platform, pyversion=sys.version)

def get_host_settings(self):
Expand Down
24 changes: 21 additions & 3 deletions kubernetes/e2e_test/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -757,9 +757,11 @@ def test_format_quantity(self):
"0.3Gi")

# == base 1000 ==
self.assertEqual(quantity.format_quantity(Decimal(0.000_000_001), "n"), "1n")
self.assertEqual(quantity.format_quantity(Decimal(0.000_001), "u"), "1u")
self.assertEqual(quantity.format_quantity(Decimal(0.001), "m"), "1m")
# Exact Decimals: float-derived Decimals (e.g. Decimal(0.001)) carry binary
# rounding error that the precise scaling surfaces into the output.
self.assertEqual(quantity.format_quantity(Decimal("0.000000001"), "n"), "1n")
self.assertEqual(quantity.format_quantity(Decimal("0.000001"), "u"), "1u")
self.assertEqual(quantity.format_quantity(Decimal("0.001"), "m"), "1m")
self.assertEqual(quantity.format_quantity(Decimal(1_000), "k"), "1k")
self.assertEqual(quantity.format_quantity(Decimal(1_000_000), "M"), "1M")
self.assertEqual(quantity.format_quantity(Decimal(1_000_000_000), "G"), "1G")
Expand All @@ -781,3 +783,19 @@ def test_format_quantity(self):
),
"333k",
)

# == milli/micro/nano keep full precision in canonical decimal form ==
# Regression: these divided by a float-built Decimal and rendered in
# scientific notation (e.g. "499.9999999999999895916591441m", "1E+9").
self.assertEqual(quantity.format_quantity(Decimal("0.5"), "m"), "500m")
self.assertEqual(quantity.format_quantity(Decimal("0.5"), "u"), "500000u")
self.assertEqual(quantity.format_quantity(Decimal("0.5"), "n"), "500000000n")
self.assertEqual(quantity.format_quantity(Decimal("1"), "m"), "1000m")
self.assertEqual(quantity.format_quantity(Decimal("1"), "n"), "1000000000n")

# == quantize=Decimal(0) is honored, not treated as falsy ==
self.assertEqual(quantity.format_quantity(Decimal("0.5006"), "m"), "500.6m")
self.assertEqual(
quantity.format_quantity(Decimal("0.5006"), "m", quantize=Decimal(0)),
"501m",
)
6 changes: 3 additions & 3 deletions kubernetes/utils/quantity.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def format_quantity(quantity_value, suffix, quantize=None) -> str:
if suffix[0] not in _EXPONENTS:
raise ValueError(f"{quantity_value} has unknown suffix")

different_scale = quantity_value / Decimal(base ** _EXPONENTS[suffix[0]])
if quantize:
different_scale = quantity_value / (Decimal(base) ** _EXPONENTS[suffix[0]])
if quantize is not None:
different_scale = different_scale.quantize(quantize)
return str(different_scale) + suffix
return format(different_scale, "f") + suffix
Loading