From ac60e8ac6b3015e755e5ff7946775e104661fff4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 03:38:37 +0000 Subject: [PATCH 1/5] Bump actions/setup-python from 4 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/e2e-master.yaml | 2 +- .github/workflows/e2e-release-11.0.yaml | 2 +- .github/workflows/e2e-release-12.0.yaml | 2 +- .github/workflows/e2e-release-17.0.yaml | 2 +- .github/workflows/e2e-release-18.0.yaml | 2 +- .github/workflows/e2e-release-26.0.yaml | 2 +- .github/workflows/test.yaml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/e2e-master.yaml b/.github/workflows/e2e-master.yaml index e40e4e5426..28aa84df0b 100644 --- a/.github/workflows/e2e-master.yaml +++ b/.github/workflows/e2e-master.yaml @@ -30,7 +30,7 @@ jobs: # as we sync with Kubernetes upstream config: .github/workflows/kind-configs/cluster-1.25.yaml - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/e2e-release-11.0.yaml b/.github/workflows/e2e-release-11.0.yaml index 0166031ca3..de7af5ff5d 100644 --- a/.github/workflows/e2e-release-11.0.yaml +++ b/.github/workflows/e2e-release-11.0.yaml @@ -30,7 +30,7 @@ jobs: # as we sync with Kubernetes upstream config: .github/workflows/kind-configs/cluster-1.15.yaml - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/e2e-release-12.0.yaml b/.github/workflows/e2e-release-12.0.yaml index 2feb83b29f..07b111f767 100644 --- a/.github/workflows/e2e-release-12.0.yaml +++ b/.github/workflows/e2e-release-12.0.yaml @@ -30,7 +30,7 @@ jobs: # as we sync with Kubernetes upstream config: .github/workflows/kind-configs/cluster-1.16.yaml - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/e2e-release-17.0.yaml b/.github/workflows/e2e-release-17.0.yaml index 0939399fb1..16bf751202 100644 --- a/.github/workflows/e2e-release-17.0.yaml +++ b/.github/workflows/e2e-release-17.0.yaml @@ -30,7 +30,7 @@ jobs: # as we sync with Kubernetes upstream config: .github/workflows/kind-configs/cluster-1.17.yaml - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/e2e-release-18.0.yaml b/.github/workflows/e2e-release-18.0.yaml index 5297063d91..5e4d97654a 100644 --- a/.github/workflows/e2e-release-18.0.yaml +++ b/.github/workflows/e2e-release-18.0.yaml @@ -30,7 +30,7 @@ jobs: # as we sync with Kubernetes upstream config: .github/workflows/kind-configs/cluster-1.18.yaml - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/e2e-release-26.0.yaml b/.github/workflows/e2e-release-26.0.yaml index 517d67590b..87c3043c33 100644 --- a/.github/workflows/e2e-release-26.0.yaml +++ b/.github/workflows/e2e-release-26.0.yaml @@ -30,7 +30,7 @@ jobs: # as we sync with Kubernetes upstream config: .github/workflows/kind-configs/cluster-1.26.yaml - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ab6e499328..04fe2f2d39 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -17,7 +17,7 @@ jobs: with: submodules: true - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 061c257701f446f1e6465157714a202afad59aa3 Mon Sep 17 00:00:00 2001 From: David E <120523989+davidopic@users.noreply.github.com> Date: Mon, 7 Aug 2023 12:20:31 -0700 Subject: [PATCH 2/5] Handled UTF-8 edge cases in Watch --- kubernetes/base/watch/watch.py | 35 +++++--- kubernetes/base/watch/watch_test.py | 120 ++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 11 deletions(-) diff --git a/kubernetes/base/watch/watch.py b/kubernetes/base/watch/watch.py index 71fd459191..f78ab11d6f 100644 --- a/kubernetes/base/watch/watch.py +++ b/kubernetes/base/watch/watch.py @@ -52,20 +52,33 @@ def _find_return_type(func): def iter_resp_lines(resp): - prev = "" - for seg in resp.stream(amt=None, decode_content=False): - if isinstance(seg, bytes): - seg = seg.decode('utf8') - seg = prev + seg - lines = seg.split("\n") - if not seg.endswith("\n"): - prev = lines[-1] - lines = lines[:-1] + buffer = bytearray() + for segment in resp.stream(amt=None, decode_content=False): + + # Append the segment (chunk) to the buffer + # + # Performance note: depending on contents of buffer and the type+value of segment, + # encoding segment into the buffer could be a wasteful step. The approach used here + # simplifies the logic farther down, but in the future it may be reasonable to + # sacrifice readability for performance. + if isinstance(segment, bytes): + buffer.extend(segment) + elif isinstance(segment, str): + buffer.extend(segment.encode("utf-8")) else: - prev = "" - for line in lines: + raise TypeError( + f"Received invalid segment type, {type(segment)}, from stream. Accepts only 'str' or 'bytes'.") + + # Split by newline (safe for utf-8 because multi-byte sequences cannot contain the newline byte) + next_newline = buffer.find(b'\n') + while next_newline != -1: + # Convert bytes to a valid utf-8 string, replacing any invalid utf-8 with the '�' character + line = buffer[:next_newline].decode( + "utf-8", errors="replace") + buffer = buffer[next_newline+1:] if line: yield line + next_newline = buffer.find(b'\n') class Watch(object): diff --git a/kubernetes/base/watch/watch_test.py b/kubernetes/base/watch/watch_test.py index 8164e7b5da..c5bc5c378c 100644 --- a/kubernetes/base/watch/watch_test.py +++ b/kubernetes/base/watch/watch_test.py @@ -61,6 +61,9 @@ def test_watch_with_decode(self): if count == 4: w.stop() + # make sure that all three records were consumed by the stream + self.assertEqual(4, count) + fake_api.get_namespaces.assert_called_once_with( _preload_content=False, watch=True) fake_resp.stream.assert_called_once_with( @@ -68,6 +71,123 @@ def test_watch_with_decode(self): fake_resp.close.assert_called_once() fake_resp.release_conn.assert_called_once() + def test_watch_with_interspersed_newlines(self): + fake_resp = Mock() + fake_resp.close = Mock() + fake_resp.release_conn = Mock() + fake_resp.stream = Mock( + return_value=[ + '\n', + '{"type": "ADDED", "object": {"metadata":', + '{"name": "test1","resourceVersion": "1"}}}\n{"type": "ADDED", ', + '"object": {"metadata": {"name": "test2", "resourceVersion": "2"}}}\n', + '\n', + '', + '{"type": "ADDED", "object": {"metadata": {"name": "test3", "resourceVersion": "3"}}}\n', + '\n\n\n', + '\n', + ]) + + fake_api = Mock() + fake_api.get_namespaces = Mock(return_value=fake_resp) + fake_api.get_namespaces.__doc__ = ':return: V1NamespaceList' + + w = Watch() + count = 0 + + # Consume all test events from the mock service, stopping when no more data is available. + # Note that "timeout_seconds" below is not a timeout; rather, it disables retries and is + # the only way to do so. Without that, the stream will re-read the test data forever. + for e in w.stream(fake_api.get_namespaces, timeout_seconds=1): + count += 1 + self.assertEqual("test%d" % count, e['object'].metadata.name) + self.assertEqual(3, count) + + def test_watch_with_multibyte_utf8(self): + fake_resp = Mock() + fake_resp.close = Mock() + fake_resp.release_conn = Mock() + fake_resp.stream = Mock( + return_value=[ + # two-byte utf-8 character + '{"type":"MODIFIED","object":{"data":{"utf-8":"© 1"},"metadata":{"name":"test1","resourceVersion":"1"}}}\n', + # same copyright character expressed as bytes + b'{"type":"MODIFIED","object":{"data":{"utf-8":"\xC2\xA9 2"},"metadata":{"name":"test2","resourceVersion":"2"}}}\n' + # same copyright character with bytes split across two stream chunks + b'{"type":"MODIFIED","object":{"data":{"utf-8":"\xC2', + b'\xA9 3"},"metadata":{"n', + # more chunks of the same event, sent as a mix of bytes and strings + 'ame":"test3","resourceVersion":"3"', + '}}}', + b'\n' + ]) + + fake_api = Mock() + fake_api.get_configmaps = Mock(return_value=fake_resp) + fake_api.get_configmaps.__doc__ = ':return: V1ConfigMapList' + + w = Watch() + count = 0 + + # Consume all test events from the mock service, stopping when no more data is available. + # Note that "timeout_seconds" below is not a timeout; rather, it disables retries and is + # the only way to do so. Without that, the stream will re-read the test data forever. + for event in w.stream(fake_api.get_configmaps, timeout_seconds=1): + count += 1 + self.assertEqual("MODIFIED", event['type']) + self.assertEqual("test%d" % count, event['object'].metadata.name) + self.assertEqual("© %d" % count, event['object'].data["utf-8"]) + self.assertEqual( + "%d" % count, event['object'].metadata.resource_version) + self.assertEqual("%d" % count, w.resource_version) + self.assertEqual(3, count) + + def test_watch_with_invalid_utf8(self): + fake_resp = Mock() + fake_resp.close = Mock() + fake_resp.release_conn = Mock() + fake_resp.stream = Mock( + # test 1 uses 1 invalid utf-8 byte + # test 2 uses a sequence of 2 invalid utf-8 bytes + # test 3 uses a sequence of 3 invalid utf-8 bytes + return_value=[ + # utf-8 sequence for 😄 is \xF0\x9F\x98\x84 + # all other sequences below are invalid + # ref: https://www.w3.org/2001/06/utf-8-wrong/UTF-8-test.html + b'{"type":"MODIFIED","object":{"data":{"utf-8":"\xF0\x9F\x98\x84 1","invalid":"\x80 1"},"metadata":{"name":"test1"}}}\n', + b'{"type":"MODIFIED","object":{"data":{"utf-8":"\xF0\x9F\x98\x84 2","invalid":"\xC0\xAF 2"},"metadata":{"name":"test2"}}}\n', + # mix bytes/strings and split byte sequences across chunks + b'{"type":"MODIFIED","object":{"data":{"utf-8":"\xF0\x9F\x98', + b'\x84 ', + b'', + b'3","invalid":"\xE0\x80', + b'\xAF ', + '3"},"metadata":{"n', + 'ame":"test3"', + '}}}', + b'\n' + ]) + + fake_api = Mock() + fake_api.get_configmaps = Mock(return_value=fake_resp) + fake_api.get_configmaps.__doc__ = ':return: V1ConfigMapList' + + w = Watch() + count = 0 + + # Consume all test events from the mock service, stopping when no more data is available. + # Note that "timeout_seconds" below is not a timeout; rather, it disables retries and is + # the only way to do so. Without that, the stream will re-read the test data forever. + for event in w.stream(fake_api.get_configmaps, timeout_seconds=1): + count += 1 + self.assertEqual("MODIFIED", event['type']) + self.assertEqual("test%d" % count, event['object'].metadata.name) + self.assertEqual("😄 %d" % count, event['object'].data["utf-8"]) + # expect N replacement characters in test N + self.assertEqual("� %d".replace('�', '�'*count) % + count, event['object'].data["invalid"]) + self.assertEqual(3, count) + def test_watch_for_follow(self): fake_resp = Mock() fake_resp.close = Mock() From 3398396f87eaff8fe0f208c63eb2d5c4e57f3ee0 Mon Sep 17 00:00:00 2001 From: yliao Date: Thu, 28 Dec 2023 17:49:24 +0000 Subject: [PATCH 3/5] update changelog with release notes from master branch --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 006f694b51..9e70721587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# v29.0.0b1 + +Kubernetes API Version: v1.29.0 + +### Bug or Regression +- Fix UTF-8 failures in Watch (#2100, @davidopic) +- Fix upper version boundary of urllib3, since other dependencies don't support urllib3 in version 2 (#2105, @jsaalfeld) + # v29.0.0a1 Kubernetes API Version: v1.29.0 From 92d44ab7a8c1658fcedc1ca10ce680cc3db6b48a Mon Sep 17 00:00:00 2001 From: yliao Date: Thu, 28 Dec 2023 17:49:25 +0000 Subject: [PATCH 4/5] update version constants for 29.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 6f431ceae7..91668c369d 100644 --- a/scripts/constants.py +++ b/scripts/constants.py @@ -18,13 +18,13 @@ KUBERNETES_BRANCH = "release-1.29" # client version for packaging and releasing. -CLIENT_VERSION = "29.0.0a1" +CLIENT_VERSION = "29.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 b697f01c31ad942b4d6185e463b72eadfd35e72e Mon Sep 17 00:00:00 2001 From: yliao Date: Thu, 28 Dec 2023 17:51:42 +0000 Subject: [PATCH 5/5] generated client change --- kubernetes/README.md | 2 +- kubernetes/__init__.py | 2 +- kubernetes/client/__init__.py | 2 +- kubernetes/client/api_client.py | 2 +- kubernetes/client/configuration.py | 2 +- setup.py | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/kubernetes/README.md b/kubernetes/README.md index 7f745b76d4..a109628c95 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.29 -- Package version: 29.0.0a1 +- Package version: 29.0.0b1 - Build package: org.openapitools.codegen.languages.PythonClientCodegen ## Requirements. diff --git a/kubernetes/__init__.py b/kubernetes/__init__.py index 42a5396de8..b0661c8905 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__ = "29.0.0a1" +__version__ = "29.0.0b1" from . import client from . import config diff --git a/kubernetes/client/__init__.py b/kubernetes/client/__init__.py index 93d933814e..78572b9776 100644 --- a/kubernetes/client/__init__.py +++ b/kubernetes/client/__init__.py @@ -14,7 +14,7 @@ from __future__ import absolute_import -__version__ = "29.0.0a1" +__version__ = "29.0.0b1" # import apis into sdk package from kubernetes.client.api.well_known_api import WellKnownApi diff --git a/kubernetes/client/api_client.py b/kubernetes/client/api_client.py index 2d9e2978df..2385393dc6 100644 --- a/kubernetes/client/api_client.py +++ b/kubernetes/client/api_client.py @@ -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/29.0.0a1/python' + self.user_agent = 'OpenAPI-Generator/29.0.0b1/python' self.client_side_validation = configuration.client_side_validation def __enter__(self): diff --git a/kubernetes/client/configuration.py b/kubernetes/client/configuration.py index 973a93d73d..555a92b040 100644 --- a/kubernetes/client/configuration.py +++ b/kubernetes/client/configuration.py @@ -354,7 +354,7 @@ def to_debug_report(self): "OS: {env}\n"\ "Python Version: {pyversion}\n"\ "Version of the API: release-1.29\n"\ - "SDK Package Version: 29.0.0a1".\ + "SDK Package Version: 29.0.0b1".\ format(env=sys.platform, pyversion=sys.version) def get_host_settings(self): diff --git a/setup.py b/setup.py index 9441b5f2ff..0de7ac73c6 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 = "29.0.0a1" +CLIENT_VERSION = "29.0.0b1" PACKAGE_NAME = "kubernetes" -DEVELOPMENT_STATUS = "3 - Alpha" +DEVELOPMENT_STATUS = "4 - Beta" # To install the library, run the following #