From cd870841460c8edd36beacb84ffa81a2efe22d1b Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 26 Mar 2026 11:39:58 +0000 Subject: [PATCH 01/33] feat: add keystoneauth_kubeservicetoken authentication plugin --- .../keystoneauth-kubeservicetoken/README.md | 71 +++ .../pyproject.toml | 40 ++ .../keystoneauth_kubeservicetoken/__init__.py | 11 + .../src/keystoneauth_kubeservicetoken/oidc.py | 67 +++ .../tests/test_oidc_access_token_file.py | 160 ++++++ python/keystoneauth-kubeservicetoken/uv.lock | 512 ++++++++++++++++++ 6 files changed, 861 insertions(+) create mode 100644 python/keystoneauth-kubeservicetoken/README.md create mode 100644 python/keystoneauth-kubeservicetoken/pyproject.toml create mode 100644 python/keystoneauth-kubeservicetoken/src/keystoneauth_kubeservicetoken/__init__.py create mode 100644 python/keystoneauth-kubeservicetoken/src/keystoneauth_kubeservicetoken/oidc.py create mode 100644 python/keystoneauth-kubeservicetoken/tests/test_oidc_access_token_file.py create mode 100644 python/keystoneauth-kubeservicetoken/uv.lock diff --git a/python/keystoneauth-kubeservicetoken/README.md b/python/keystoneauth-kubeservicetoken/README.md new file mode 100644 index 000000000..1bb4350d6 --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/README.md @@ -0,0 +1,71 @@ +# File-backed OIDC access token plugin + +This package provides a `keystoneauth1` plugin that extends the OIDC +access-token flow by reading the OIDC access token from a file +(`access_token_file`) at authentication and reauthentication time. + +## Auth type + +- `v3oidcaccesstokenfile` + +## Required options + +- `auth_url` +- `identity_provider` +- `protocol` +- `access_token_file` + +## Service configuration examples + +### Nova (`nova.conf`) + +```ini +[service_user] +auth_type = v3oidcaccesstokenfile +auth_url = https://keystone.example/v3 +identity_provider = k8s-workload-idp +protocol = openid +access_token_file = /var/run/secrets/openstack/nova-oidc-token +send_service_user_token = true +``` + +### Ironic -> Neutron client (`ironic.conf`) + +```ini +[neutron] +auth_type = v3oidcaccesstokenfile +auth_url = https://keystone.internal:5000/v3 +identity_provider = k8s-workload-idp +protocol = openid +access_token_file = /var/run/secrets/openstack/ironic-oidc-token +region_name = RegionOne +``` + +### Neutron -> Placement client (`neutron.conf`) + +```ini +[placement] +auth_type = v3oidcaccesstokenfile +auth_url = https://keystone.example/v3 +identity_provider = k8s-workload-idp +protocol = openid +access_token_file = /var/run/secrets/openstack/neutron-placement-oidc-token +valid_interfaces = internal +``` + +## Behavior notes + +- Token content is read from file on authentication and reauthentication. +- Whitespace in the token file is trimmed. +- Missing, unreadable, or empty token files fail with an explicit auth error. +- Keystone token caching is preserved; token file updates are consumed when + keystoneauth reauthenticates near Keystone token expiry. + +## Rollout notes + +1. Install this package where the OpenStack service runs. +2. Configure the service to use `auth_type = v3oidcaccesstokenfile`. +3. Set `access_token_file` to the rotated token file path. This will usually be + path to where the Kubernetes secret is mounted. +4. Optionally verify at least one full Keystone token renewal cycle to confirm + file updates are consumed. diff --git a/python/keystoneauth-kubeservicetoken/pyproject.toml b/python/keystoneauth-kubeservicetoken/pyproject.toml new file mode 100644 index 000000000..b8f99a511 --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/pyproject.toml @@ -0,0 +1,40 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "keystoneauth-kubeservicetoken" +version = "0.1.0" +description = "Keystoneauth plugin that reads OIDC access tokens from a file" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "keystoneauth1>=5.0.0", +] + +[dependency-groups] +dev = [ + "pytest>=8.0.0", + "pytest-cov>=7.1.0", +] + +[project.entry-points."keystoneauth1.plugin"] +v3oidcaccesstokenfile = "keystoneauth_kubeservicetoken.oidc:OpenIDConnectAccessTokenFileLoader" + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.ruff] +line-length = 88 + +[tool.ruff.lint] +select = ["E", "F", "B", "I", "UP", "S"] + +[tool.ruff.lint.per-file-ignores] +"tests/*.py" = ["S101", "S106"] diff --git a/python/keystoneauth-kubeservicetoken/src/keystoneauth_kubeservicetoken/__init__.py b/python/keystoneauth-kubeservicetoken/src/keystoneauth_kubeservicetoken/__init__.py new file mode 100644 index 000000000..0e85b8e7f --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/src/keystoneauth_kubeservicetoken/__init__.py @@ -0,0 +1,11 @@ +"""keystoneauth_kubeservicetoken package.""" + +from keystoneauth_kubeservicetoken.oidc import ( + OpenIDConnectAccessTokenFile, + OpenIDConnectAccessTokenFileLoader, +) + +__all__ = [ + "OpenIDConnectAccessTokenFile", + "OpenIDConnectAccessTokenFileLoader", +] diff --git a/python/keystoneauth-kubeservicetoken/src/keystoneauth_kubeservicetoken/oidc.py b/python/keystoneauth-kubeservicetoken/src/keystoneauth_kubeservicetoken/oidc.py new file mode 100644 index 000000000..701582fee --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/src/keystoneauth_kubeservicetoken/oidc.py @@ -0,0 +1,67 @@ +"""File-backed OIDC access token plugin for keystoneauth.""" + +from __future__ import annotations + +from keystoneauth1 import exceptions, loading +from keystoneauth1.identity.v3 import oidc +from keystoneauth1.loading._plugins.identity import v3 as identity_v3_loading + + +class OpenIDConnectAccessTokenFile(oidc.OidcAccessToken): + """OIDC access-token auth plugin that reads the token from a file.""" + + def __init__( + self, *args, access_token_file: str, access_token: str | None = None, **kwargs + ): + if not access_token_file: + raise exceptions.OptionError("'access_token_file' is required") + + self.access_token_file = access_token_file + + super().__init__(*args, access_token=access_token or "", **kwargs) + + def _read_access_token(self) -> str: + try: + with open(self.access_token_file, encoding="utf-8") as token_file: + token = token_file.read().strip() + except FileNotFoundError as exc: + msg = f"OIDC access token file does not exist: {self.access_token_file}" + raise exceptions.AuthorizationFailure(msg) from exc + except OSError as exc: + msg = ( + "Unable to read OIDC access token file " + f"'{self.access_token_file}': {exc}" + ) + raise exceptions.AuthorizationFailure(msg) from exc + + if not token: + msg = f"OIDC access token file is empty: {self.access_token_file}" + raise exceptions.AuthorizationFailure(msg) + + return token + + def get_unscoped_auth_ref(self, session): + self.access_token = self._read_access_token() + return super().get_unscoped_auth_ref(session) + + +class OpenIDConnectAccessTokenFileLoader(identity_v3_loading.OpenIDConnectAccessToken): + """Loader for the file-backed OIDC access-token auth plugin.""" + + @property + def plugin_class(self): + return OpenIDConnectAccessTokenFile + + def get_options(self): + options = [ + option for option in super().get_options() if option.dest != "access_token" + ] + + options.append( + loading.Opt( + "access-token-file", + required=True, + help="Path to a file containing the OIDC access token.", + ) + ) + return options diff --git a/python/keystoneauth-kubeservicetoken/tests/test_oidc_access_token_file.py b/python/keystoneauth-kubeservicetoken/tests/test_oidc_access_token_file.py new file mode 100644 index 000000000..f94db6b92 --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/tests/test_oidc_access_token_file.py @@ -0,0 +1,160 @@ +from __future__ import annotations + +from pathlib import Path +from unittest.mock import patch + +import pytest +from keystoneauth1 import exceptions, loading +from keystoneauth1.identity.v3 import oidc as upstream_oidc + +from keystoneauth_kubeservicetoken.oidc import ( + OpenIDConnectAccessTokenFile, + OpenIDConnectAccessTokenFileLoader, +) + + +class FakeAuthRef: + def __init__(self, auth_token: str, expires_soon: bool): + self.auth_token = auth_token + self._expires_soon = expires_soon + + def will_expire_soon(self, _stale_duration: int) -> bool: + return self._expires_soon + + +def _create_plugin(token_file: Path) -> OpenIDConnectAccessTokenFile: + return OpenIDConnectAccessTokenFile( + auth_url="https://keystone.example/v3", + identity_provider="example-idp", + protocol="openid", + access_token_file=str(token_file), + ) + + +@pytest.fixture +def auth_options() -> dict[str, str]: + return { + "auth_url": "https://keystone.example/v3", + "identity_provider": "example-idp", + "protocol": "openid", + } + + +def test_loader_options_require_access_token_file_and_not_access_token(): + loader = OpenIDConnectAccessTokenFileLoader() + + options = {option.dest: option for option in loader.get_options()} + + assert "access_token_file" in options + assert options["access_token_file"].required + assert "access_token" not in options + + +def test_loader_can_initialize_plugin_with_access_token_file_only( + tmp_path, auth_options +): + token_file = tmp_path / "token" + token_file.write_text("oidc-token", encoding="utf-8") + + loader = OpenIDConnectAccessTokenFileLoader() + plugin = loader.load_from_options( + **auth_options, + access_token_file=str(token_file), + ) + + assert isinstance(plugin, OpenIDConnectAccessTokenFile) + assert plugin.access_token_file == str(token_file) + + +def test_plugin_loader_is_discoverable_by_auth_type(): + loader = loading.get_plugin_loader("v3oidcaccesstokenfile") + + assert isinstance(loader, OpenIDConnectAccessTokenFileLoader) + + +def test_missing_access_token_file_configuration_fails(auth_options): + with pytest.raises(exceptions.OptionError, match="access_token_file"): + OpenIDConnectAccessTokenFile( + **auth_options, + access_token_file="", + ) + + +def test_auth_reads_trimmed_token_from_file_each_time(tmp_path): + token_file = tmp_path / "token" + token_file.write_text(" token-a\n", encoding="utf-8") + + plugin = _create_plugin(token_file) + observed_tokens: list[str] = [] + + def fake_super_get_unscoped_auth_ref(self, _session): + observed_tokens.append(self.access_token) + return object() + + with patch.object( + upstream_oidc.OidcAccessToken, + "get_unscoped_auth_ref", + autospec=True, + side_effect=fake_super_get_unscoped_auth_ref, + ): + plugin.get_unscoped_auth_ref(session=None) + token_file.write_text("token-b\n", encoding="utf-8") + plugin.get_unscoped_auth_ref(session=None) + + assert observed_tokens == ["token-a", "token-b"] + + +def test_auth_fails_for_missing_file(tmp_path): + plugin = _create_plugin(tmp_path / "missing-token") + + with pytest.raises(exceptions.AuthorizationFailure, match="does not exist"): + plugin._read_access_token() + + +def test_auth_fails_for_unreadable_file(tmp_path): + token_file = tmp_path / "token" + token_file.write_text("token", encoding="utf-8") + plugin = _create_plugin(token_file) + + with patch("builtins.open", side_effect=OSError("permission denied")): + with pytest.raises(exceptions.AuthorizationFailure, match="Unable to read"): + plugin._read_access_token() + + +def test_auth_fails_for_empty_file(tmp_path): + token_file = tmp_path / "token" + token_file.write_text("\n\t", encoding="utf-8") + plugin = _create_plugin(token_file) + + with pytest.raises(exceptions.AuthorizationFailure, match="is empty"): + plugin._read_access_token() + + +def test_file_updates_consumed_on_reauth_not_every_request(tmp_path): + token_file = tmp_path / "token" + token_file.write_text("token-a", encoding="utf-8") + plugin = _create_plugin(token_file) + + plugin.auth_ref = FakeAuthRef( + auth_token="cached-keystone-token", expires_soon=False + ) + + def fake_get_auth_ref(_session): + plugin.get_unscoped_auth_ref(session=None) + return FakeAuthRef(auth_token=f"ks-{plugin.access_token}", expires_soon=False) + + with ( + patch.object( + upstream_oidc.OidcAccessToken, + "get_unscoped_auth_ref", + autospec=True, + return_value=object(), + ), + patch.object(plugin, "get_auth_ref", side_effect=fake_get_auth_ref), + ): + token_file.write_text("token-b", encoding="utf-8") + + assert plugin.get_token(session=None) == "cached-keystone-token" + + plugin.auth_ref = FakeAuthRef(auth_token="expiring-token", expires_soon=True) + assert plugin.get_token(session=None) == "ks-token-b" diff --git a/python/keystoneauth-kubeservicetoken/uv.lock b/python/keystoneauth-kubeservicetoken/uv.lock new file mode 100644 index 000000000..24f9f4370 --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/uv.lock @@ -0,0 +1,512 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/8c/2c56124c6dc53a774d435f985b5973bc592f42d437be58c0c92d65ae7296/charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95", size = 298751, upload-time = "2026-03-15T18:50:00.003Z" }, + { url = "https://files.pythonhosted.org/packages/86/2a/2a7db6b314b966a3bcad8c731c0719c60b931b931de7ae9f34b2839289ee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd", size = 200027, upload-time = "2026-03-15T18:50:01.702Z" }, + { url = "https://files.pythonhosted.org/packages/68/f2/0fe775c74ae25e2a3b07b01538fc162737b3e3f795bada3bc26f4d4d495c/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4", size = 220741, upload-time = "2026-03-15T18:50:03.194Z" }, + { url = "https://files.pythonhosted.org/packages/10/98/8085596e41f00b27dd6aa1e68413d1ddda7e605f34dd546833c61fddd709/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db", size = 215802, upload-time = "2026-03-15T18:50:05.859Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ce/865e4e09b041bad659d682bbd98b47fb490b8e124f9398c9448065f64fee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89", size = 207908, upload-time = "2026-03-15T18:50:07.676Z" }, + { url = "https://files.pythonhosted.org/packages/a8/54/8c757f1f7349262898c2f169e0d562b39dcb977503f18fdf0814e923db78/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565", size = 194357, upload-time = "2026-03-15T18:50:09.327Z" }, + { url = "https://files.pythonhosted.org/packages/6f/29/e88f2fac9218907fc7a70722b393d1bbe8334c61fe9c46640dba349b6e66/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9", size = 205610, upload-time = "2026-03-15T18:50:10.732Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c5/21d7bb0cb415287178450171d130bed9d664211fdd59731ed2c34267b07d/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7", size = 203512, upload-time = "2026-03-15T18:50:12.535Z" }, + { url = "https://files.pythonhosted.org/packages/a4/be/ce52f3c7fdb35cc987ad38a53ebcef52eec498f4fb6c66ecfe62cfe57ba2/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550", size = 195398, upload-time = "2026-03-15T18:50:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/81/a0/3ab5dd39d4859a3555e5dadfc8a9fa7f8352f8c183d1a65c90264517da0e/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0", size = 221772, upload-time = "2026-03-15T18:50:15.581Z" }, + { url = "https://files.pythonhosted.org/packages/04/6e/6a4e41a97ba6b2fa87f849c41e4d229449a586be85053c4d90135fe82d26/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8", size = 205759, upload-time = "2026-03-15T18:50:17.047Z" }, + { url = "https://files.pythonhosted.org/packages/db/3b/34a712a5ee64a6957bf355b01dc17b12de457638d436fdb05d01e463cd1c/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0", size = 216938, upload-time = "2026-03-15T18:50:18.44Z" }, + { url = "https://files.pythonhosted.org/packages/cb/05/5bd1e12da9ab18790af05c61aafd01a60f489778179b621ac2a305243c62/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b", size = 210138, upload-time = "2026-03-15T18:50:19.852Z" }, + { url = "https://files.pythonhosted.org/packages/bd/8e/3cb9e2d998ff6b21c0a1860343cb7b83eba9cdb66b91410e18fc4969d6ab/charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557", size = 144137, upload-time = "2026-03-15T18:50:21.505Z" }, + { url = "https://files.pythonhosted.org/packages/d8/8f/78f5489ffadb0db3eb7aff53d31c24531d33eb545f0c6f6567c25f49a5ff/charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6", size = 154244, upload-time = "2026-03-15T18:50:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/e4/74/e472659dffb0cadb2f411282d2d76c60da1fc94076d7fffed4ae8a93ec01/charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058", size = 143312, upload-time = "2026-03-15T18:50:24.074Z" }, + { url = "https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e", size = 293582, upload-time = "2026-03-15T18:50:25.454Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b7/b1a117e5385cbdb3205f6055403c2a2a220c5ea80b8716c324eaf75c5c95/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9", size = 197240, upload-time = "2026-03-15T18:50:27.196Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/2574f0f09f3c3bc1b2f992e20bce6546cb1f17e111c5be07308dc5427956/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d", size = 217363, upload-time = "2026-03-15T18:50:28.601Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d1/0ae20ad77bc949ddd39b51bf383b6ca932f2916074c95cad34ae465ab71f/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de", size = 212994, upload-time = "2026-03-15T18:50:30.102Z" }, + { url = "https://files.pythonhosted.org/packages/60/ac/3233d262a310c1b12633536a07cde5ddd16985e6e7e238e9f3f9423d8eb9/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73", size = 204697, upload-time = "2026-03-15T18:50:31.654Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/8a18fc411f085b82303cfb7154eed5bd49c77035eb7608d049468b53f87c/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c", size = 191673, upload-time = "2026-03-15T18:50:33.433Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a7/11cfe61d6c5c5c7438d6ba40919d0306ed83c9ab957f3d4da2277ff67836/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc", size = 201120, upload-time = "2026-03-15T18:50:35.105Z" }, + { url = "https://files.pythonhosted.org/packages/b5/10/cf491fa1abd47c02f69687046b896c950b92b6cd7337a27e6548adbec8e4/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f", size = 200911, upload-time = "2026-03-15T18:50:36.819Z" }, + { url = "https://files.pythonhosted.org/packages/28/70/039796160b48b18ed466fde0af84c1b090c4e288fae26cd674ad04a2d703/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef", size = 192516, upload-time = "2026-03-15T18:50:38.228Z" }, + { url = "https://files.pythonhosted.org/packages/ff/34/c56f3223393d6ff3124b9e78f7de738047c2d6bc40a4f16ac0c9d7a1cb3c/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398", size = 218795, upload-time = "2026-03-15T18:50:39.664Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3b/ce2d4f86c5282191a041fdc5a4ce18f1c6bd40a5bd1f74cf8625f08d51c1/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e", size = 201833, upload-time = "2026-03-15T18:50:41.552Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9b/b6a9f76b0fd7c5b5ec58b228ff7e85095370282150f0bd50b3126f5506d6/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed", size = 213920, upload-time = "2026-03-15T18:50:43.33Z" }, + { url = "https://files.pythonhosted.org/packages/ae/98/7bc23513a33d8172365ed30ee3a3b3fe1ece14a395e5fc94129541fc6003/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021", size = 206951, upload-time = "2026-03-15T18:50:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/32/73/c0b86f3d1458468e11aec870e6b3feac931facbe105a894b552b0e518e79/charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e", size = 143703, upload-time = "2026-03-15T18:50:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e3/76f2facfe8eddee0bbd38d2594e709033338eae44ebf1738bcefe0a06185/charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4", size = 153857, upload-time = "2026-03-15T18:50:47.563Z" }, + { url = "https://files.pythonhosted.org/packages/e2/dc/9abe19c9b27e6cd3636036b9d1b387b78c40dedbf0b47f9366737684b4b0/charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316", size = 142751, upload-time = "2026-03-15T18:50:49.234Z" }, + { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, + { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, + { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, + { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, + { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, + { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, + { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, + { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, + { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, + { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" }, + { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" }, + { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" }, + { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" }, + { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" }, + { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, + { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "iso8601" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/f3/ef59cee614d5e0accf6fd0cbba025b93b272e626ca89fb70a3e9187c5d15/iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df", size = 6522, upload-time = "2023-10-03T00:25:39.317Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/0c/f37b6a241f0759b7653ffa7213889d89ad49a2b76eb2ddf3b57b2738c347/iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242", size = 7545, upload-time = "2023-10-03T00:25:32.304Z" }, +] + +[[package]] +name = "keystoneauth-kubeservicetoken" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "keystoneauth1" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [{ name = "keystoneauth1", specifier = ">=5.0.0" }] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.0.0" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, +] + +[[package]] +name = "keystoneauth1" +version = "5.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "iso8601" }, + { name = "os-service-types" }, + { name = "pbr" }, + { name = "requests" }, + { name = "stevedore" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/bc/d99872ca0bc8bf5f248b50e3d7386dedec5278f8dd989a2a981d329a8069/keystoneauth1-5.13.1.tar.gz", hash = "sha256:e011e47ac3f3c671ffae33505c095548650cc19dab7f6af3b2ea5bd18c98f0c9", size = 288548, upload-time = "2026-02-20T15:16:47.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/68/ff03933cff7568277e2d55cb33aeb60a5c2d77bd88b0ea139244079df638/keystoneauth1-5.13.1-py3-none-any.whl", hash = "sha256:6e6d0296bc341e5f9a08e985f7083206c93a4ffd999933c2d1b7e1c833c2e501", size = 343539, upload-time = "2026-02-20T15:16:45.873Z" }, +] + +[[package]] +name = "os-service-types" +version = "1.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pbr" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/62/31e39aa8f2ac5bff0b061ce053f0610c9fe659e12aeca20bfb26d1665024/os_service_types-1.8.2.tar.gz", hash = "sha256:ab7648d7232849943196e1bb00a30e2e25e600fa3b57bb241d15b7f521b5b575", size = 27476, upload-time = "2025-11-21T13:55:47.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/26/0937af7b4383f1eba5bca789b8d191c0e09e59bb64962b18f4a14534ce41/os_service_types-1.8.2-py3-none-any.whl", hash = "sha256:f78890d71814deffabf0ed4358288ec2ced579bc4d0bb87a79ae806cbb4deb6e", size = 24876, upload-time = "2025-11-21T13:55:46.093Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pbr" +version = "7.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/ab/1de9a4f730edde1bdbbc2b8d19f8fa326f036b4f18b2f72cfbea7dc53c26/pbr-7.0.3.tar.gz", hash = "sha256:b46004ec30a5324672683ec848aed9e8fc500b0d261d40a3229c2d2bbfcedc29", size = 135625, upload-time = "2025-11-03T17:04:56.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/db/61efa0d08a99f897ef98256b03e563092d36cc38dc4ebe4a85020fe40b31/pbr-7.0.3-py2.py3-none-any.whl", hash = "sha256:ff223894eb1cd271a98076b13d3badff3bb36c424074d26334cd25aebeecea6b", size = 131898, upload-time = "2025-11-03T17:04:54.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "requests" +version = "2.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, +] + +[[package]] +name = "stevedore" +version = "5.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6d/90764092216fa560f6587f83bb70113a8ba510ba436c6476a2b47359057c/stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3", size = 516200, upload-time = "2026-02-20T13:27:06.765Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/06/36d260a695f383345ab5bbc3fd447249594ae2fa8dfd19c533d5ae23f46b/stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed", size = 54483, upload-time = "2026-02-20T13:27:05.561Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] From 8515edf62b9d9bfda68424a0db808332b3fffd32 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 12 May 2026 08:15:33 +0100 Subject: [PATCH 02/33] feat: add oidc-rbac component for service account issuer discovery Allow unauthenticated access to OIDC discovery endpoints by deploying the oidc-reviewer ClusterRoleBinding to all site and global clusters. --- .../templates/application-oidc-rbac.yaml | 30 +++++++++++++ charts/argocd-understack/values.yaml | 12 +++++ components/oidc-rbac/kustomization.yaml | 5 +++ components/oidc-rbac/oidc-reviewer.yaml | 12 +++++ docs/deploy-guide/components/oidc-rbac.md | 44 +++++++++++++++++++ properdocs.yml | 1 + 6 files changed, 104 insertions(+) create mode 100644 charts/argocd-understack/templates/application-oidc-rbac.yaml create mode 100644 components/oidc-rbac/kustomization.yaml create mode 100644 components/oidc-rbac/oidc-reviewer.yaml create mode 100644 docs/deploy-guide/components/oidc-rbac.md diff --git a/charts/argocd-understack/templates/application-oidc-rbac.yaml b/charts/argocd-understack/templates/application-oidc-rbac.yaml new file mode 100644 index 000000000..0e0db3b8a --- /dev/null +++ b/charts/argocd-understack/templates/application-oidc-rbac.yaml @@ -0,0 +1,30 @@ +{{- if or (eq (include "understack.isEnabled" (list $.Values.global "oidc_rbac")) "true") (eq (include "understack.isEnabled" (list $.Values.site "oidc_rbac")) "true") }} +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: {{ printf "%s-%s" $.Release.Name "oidc-rbac" }} + finalizers: + - resources-finalizer.argocd.argoproj.io + annotations: + argocd.argoproj.io/compare-options: ServerSideDiff=true,IncludeMutationWebhook=true +{{- include "understack.appLabelsBlock" $ | nindent 2 }} +spec: + destination: + namespace: kube-system + server: {{ $.Values.cluster_server }} + project: understack-infra + sources: + - path: components/oidc-rbac + ref: understack + repoURL: {{ include "understack.understack_url" $ }} + targetRevision: {{ include "understack.understack_ref" $ }} + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - ServerSideApply=true + - RespectIgnoreDifferences=true + - ApplyOutOfSyncOnly=true +{{- end }} diff --git a/charts/argocd-understack/values.yaml b/charts/argocd-understack/values.yaml index abf3a677f..956a50d80 100644 --- a/charts/argocd-understack/values.yaml +++ b/charts/argocd-understack/values.yaml @@ -169,6 +169,12 @@ global: # @default -- 0.0.3 chartVersion: 0.0.3 + # -- OIDC RBAC (ClusterRoleBindings for OIDC service account issuer discovery) + oidc_rbac: + # -- Enable/disable deploying OIDC RBAC + # @default -- false + enabled: false + # -- OpenEBS openebs: # -- Enable/disable deploying OpenEBS @@ -508,6 +514,12 @@ site: # @default -- false enabled: false + # -- OIDC RBAC (ClusterRoleBindings for OIDC service account issuer discovery) + oidc_rbac: + # -- Enable/disable deploying OIDC RBAC + # @default -- false + enabled: false + # -- OpenEBS openebs: # -- Enable/disable deploying OpenEBS diff --git a/components/oidc-rbac/kustomization.yaml b/components/oidc-rbac/kustomization.yaml new file mode 100644 index 000000000..51b635bc8 --- /dev/null +++ b/components/oidc-rbac/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - oidc-reviewer.yaml diff --git a/components/oidc-rbac/oidc-reviewer.yaml b/components/oidc-rbac/oidc-reviewer.yaml new file mode 100644 index 000000000..360f13ea3 --- /dev/null +++ b/components/oidc-rbac/oidc-reviewer.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: oidc-reviewer +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:service-account-issuer-discovery +subjects: +- apiGroup: rbac.authorization.k8s.io + kind: Group + name: system:unauthenticated diff --git a/docs/deploy-guide/components/oidc-rbac.md b/docs/deploy-guide/components/oidc-rbac.md new file mode 100644 index 000000000..bce9280d6 --- /dev/null +++ b/docs/deploy-guide/components/oidc-rbac.md @@ -0,0 +1,44 @@ +--- +kustomize_paths: +- components/oidc-rbac +deploy_overrides: + helm: + mode: none + kustomize: + mode: none +--- + +# oidc-rbac + +OIDC RBAC configuration for Kubernetes service account issuer discovery. + +## Deployment Scope + +- Cluster scope: global, site +- Values key: `global.oidc_rbac`, `site.oidc_rbac` +- ArgoCD Application template: `charts/argocd-understack/templates/application-oidc-rbac.yaml` + +## How ArgoCD Builds It + +{{ component_argocd_builds() }} + +## How to Enable + +This component is enabled by default. To disable it: + +```yaml title="$CLUSTER_NAME/deploy.yaml" +global: + oidc_rbac: + enabled: false +site: + oidc_rbac: + enabled: false +``` + +## Deployment Repo Content + +{{ secrets_disclaimer }} + +Required or commonly required items: + +- None for this Application today. It deploys the shared `components/oidc-rbac` base directly and does not consume deploy-repo values or overlay manifests. diff --git a/properdocs.yml b/properdocs.yml index 23b373a76..66c4e65a3 100644 --- a/properdocs.yml +++ b/properdocs.yml @@ -199,6 +199,7 @@ nav: - deploy-guide/components/neutron.md - deploy-guide/components/nova.md - deploy-guide/components/octavia.md + - deploy-guide/components/oidc-rbac.md - deploy-guide/components/openebs.md - deploy-guide/components/openstack-exporter.md - deploy-guide/components/openstack-helm.md From 023f89dc8e37e8d43ae8e4d7ea93420379951b6b Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 13 May 2026 17:01:27 +0100 Subject: [PATCH 03/33] feat: install keystoneauth-kubeservicetoken in OpenStack containers --- containers/cinder/Dockerfile | 4 +++- containers/glance/Dockerfile | 15 +++++++++++++++ containers/horizon/Dockerfile | 15 +++++++++++++++ containers/neutron/Dockerfile | 4 +++- containers/nova/Dockerfile | 20 ++++++++++++++++++-- containers/octavia/Dockerfile | 5 ++++- containers/placement/Dockerfile | 15 +++++++++++++++ containers/skyline/Dockerfile | 15 +++++++++++++++ 8 files changed, 88 insertions(+), 5 deletions(-) diff --git a/containers/cinder/Dockerfile b/containers/cinder/Dockerfile index 1b76fff31..c6f9b3b13 100644 --- a/containers/cinder/Dockerfile +++ b/containers/cinder/Dockerfile @@ -12,12 +12,14 @@ RUN apt-get update && \ && apt-get clean && rm -rf /var/lib/apt/lists/* COPY python/cinder-understack /src/cinder-understack +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken ARG OPENSTACK_VERSION="required_argument" RUN --mount=type=cache,target=/root/.cache/uv \ uv pip install \ --constraint https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} \ - /src/cinder-understack + /src/cinder-understack \ + /src/keystoneauth-kubeservicetoken COPY containers/cinder/patches /tmp/patches/ RUN cd /var/lib/openstack/lib/python3.12/site-packages && \ diff --git a/containers/glance/Dockerfile b/containers/glance/Dockerfile index 85d6b3a6f..434350af1 100644 --- a/containers/glance/Dockerfile +++ b/containers/glance/Dockerfile @@ -1,4 +1,19 @@ # syntax=docker/dockerfile:1 +ARG OPENSTACK_VERSION="required_argument" +FROM quay.io/airshipit/glance:${OPENSTACK_VERSION}-ubuntu_noble AS build + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken + +ARG OPENSTACK_VERSION="required_argument" +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install \ + --constraint https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} \ + /src/keystoneauth-kubeservicetoken + ARG OPENSTACK_VERSION="required_argument" FROM quay.io/airshipit/glance:${OPENSTACK_VERSION}-ubuntu_noble AS final + +COPY --from=build --link /var/lib/openstack /var/lib/openstack diff --git a/containers/horizon/Dockerfile b/containers/horizon/Dockerfile index f8b18402c..bc46b99ff 100644 --- a/containers/horizon/Dockerfile +++ b/containers/horizon/Dockerfile @@ -1,4 +1,19 @@ # syntax=docker/dockerfile:1 +ARG OPENSTACK_VERSION="required_argument" +FROM quay.io/airshipit/horizon:${OPENSTACK_VERSION}-ubuntu_noble AS build + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken + +ARG OPENSTACK_VERSION="required_argument" +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install \ + --constraint https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} \ + /src/keystoneauth-kubeservicetoken + ARG OPENSTACK_VERSION="required_argument" FROM quay.io/airshipit/horizon:${OPENSTACK_VERSION}-ubuntu_noble AS final + +COPY --from=build --link /var/lib/openstack /var/lib/openstack diff --git a/containers/neutron/Dockerfile b/containers/neutron/Dockerfile index da7fd1a0c..619998379 100644 --- a/containers/neutron/Dockerfile +++ b/containers/neutron/Dockerfile @@ -16,6 +16,7 @@ ADD --keep-git-dir=true https://github.com/rackerlabs/neutron-lib.git#${NEUTRON_ RUN git -C /src/neutron-lib fetch --unshallow --tags COPY python/neutron-understack /src/neutron-understack +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken ARG OPENSTACK_VERSION="required_argument" ADD https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} /upper-constraints.txt @@ -27,7 +28,8 @@ RUN --mount=type=cache,target=/root/.cache/uv \ --constraint /upper-constraints.txt \ /src/neutron \ /src/neutron-lib \ - /src/neutron-understack + /src/neutron-understack \ + /src/keystoneauth-kubeservicetoken ARG OPENSTACK_VERSION="required_argument" FROM quay.io/airshipit/neutron:${OPENSTACK_VERSION}-ubuntu_noble AS final diff --git a/containers/nova/Dockerfile b/containers/nova/Dockerfile index 0c6ba3fae..4a4bc8e1a 100644 --- a/containers/nova/Dockerfile +++ b/containers/nova/Dockerfile @@ -1,7 +1,9 @@ # syntax=docker/dockerfile:1 ARG OPENSTACK_VERSION="required_argument" -FROM quay.io/airshipit/nova:${OPENSTACK_VERSION}-ubuntu_noble +FROM quay.io/airshipit/nova:${OPENSTACK_VERSION}-ubuntu_noble AS build + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ RUN apt-get update && \ apt-get install -y --no-install-recommends \ @@ -12,4 +14,18 @@ RUN apt-get update && \ COPY containers/nova/patches /tmp/patches/ RUN cd /var/lib/openstack/lib/python3.12/site-packages && \ QUILT_PATCHES=/tmp/patches quilt push -a -COPY python/nova-understack/ironic_understack /var/lib/openstack/lib/python3.12/site-packages/nova/virt/ironic_understack + +COPY python/nova-understack/ironic_understack /var/lib/openstack/lib/python3.12/site-packages/nova/virt/ironic_understack + +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken + +ARG OPENSTACK_VERSION="required_argument" +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install \ + --constraint https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} \ + /src/keystoneauth-kubeservicetoken + +ARG OPENSTACK_VERSION="required_argument" +FROM quay.io/airshipit/nova:${OPENSTACK_VERSION}-ubuntu_noble AS final + +COPY --from=build --link /var/lib/openstack /var/lib/openstack diff --git a/containers/octavia/Dockerfile b/containers/octavia/Dockerfile index 873a3b13d..0f4e2f10d 100644 --- a/containers/octavia/Dockerfile +++ b/containers/octavia/Dockerfile @@ -5,12 +5,15 @@ FROM quay.io/airshipit/octavia:${OPENSTACK_VERSION}-ubuntu_noble AS build COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken + ARG OPENSTACK_VERSION="required_argument" RUN --mount=type=cache,target=/root/.cache/uv \ uv pip install \ --constraint https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} \ ovn-octavia-provider \ - ovsdbapp + ovsdbapp \ + /src/keystoneauth-kubeservicetoken ARG OPENSTACK_VERSION="required_argument" FROM quay.io/airshipit/octavia:${OPENSTACK_VERSION}-ubuntu_noble AS final diff --git a/containers/placement/Dockerfile b/containers/placement/Dockerfile index 110f69393..12130e482 100644 --- a/containers/placement/Dockerfile +++ b/containers/placement/Dockerfile @@ -1,4 +1,19 @@ # syntax=docker/dockerfile:1 +ARG OPENSTACK_VERSION="required_argument" +FROM quay.io/airshipit/placement:${OPENSTACK_VERSION}-ubuntu_noble AS build + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken + +ARG OPENSTACK_VERSION="required_argument" +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install \ + --constraint https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} \ + /src/keystoneauth-kubeservicetoken + ARG OPENSTACK_VERSION="required_argument" FROM quay.io/airshipit/placement:${OPENSTACK_VERSION}-ubuntu_noble AS final + +COPY --from=build --link /var/lib/openstack /var/lib/openstack diff --git a/containers/skyline/Dockerfile b/containers/skyline/Dockerfile index 3e01e5e31..620babb41 100644 --- a/containers/skyline/Dockerfile +++ b/containers/skyline/Dockerfile @@ -1,4 +1,19 @@ # syntax=docker/dockerfile:1 +ARG OPENSTACK_VERSION="required_argument" +FROM quay.io/airshipit/skyline:${OPENSTACK_VERSION}-ubuntu_noble AS build + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken + +ARG OPENSTACK_VERSION="required_argument" +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install \ + --constraint https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} \ + /src/keystoneauth-kubeservicetoken + ARG OPENSTACK_VERSION="required_argument" FROM quay.io/airshipit/skyline:${OPENSTACK_VERSION}-ubuntu_noble AS final + +COPY --from=build --link /var/lib/openstack /var/lib/openstack From 7c646aecba9801675154db7ef38bef6b8d6e8324 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 13 May 2026 18:21:00 +0100 Subject: [PATCH 04/33] drop! change to test OpenStack images --- components/images-openstack.yaml | 136 +++++++++++++++---------------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/components/images-openstack.yaml b/components/images-openstack.yaml index d63aa6968..7a8322250 100644 --- a/components/images-openstack.yaml +++ b/components/images-openstack.yaml @@ -6,66 +6,66 @@ images: tags: # these are common across all these OpenStack Helm installations bootstrap: "ghcr.io/rackerlabs/understack/ansible:latest" - db_init: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" - db_drop: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" - ks_user: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" - ks_service: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" - ks_endpoints: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" + db_init: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" + db_drop: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" + ks_user: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" + ks_service: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" + ks_endpoints: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" # keystone - keystone_api: "ghcr.io/rackerlabs/understack/keystone:2026.1" - keystone_credential_rotate: "ghcr.io/rackerlabs/understack/keystone:2026.1" - keystone_credential_setup: "ghcr.io/rackerlabs/understack/keystone:2026.1" - keystone_db_sync: "ghcr.io/rackerlabs/understack/keystone:2026.1" - keystone_domain_manage: "ghcr.io/rackerlabs/understack/keystone:2026.1" - keystone_fernet_rotate: "ghcr.io/rackerlabs/understack/keystone:2026.1" - keystone_fernet_setup: "ghcr.io/rackerlabs/understack/keystone:2026.1" + keystone_api: "ghcr.io/rackerlabs/understack/keystone:2025.2" + keystone_credential_rotate: "ghcr.io/rackerlabs/understack/keystone:2025.2" + keystone_credential_setup: "ghcr.io/rackerlabs/understack/keystone:2025.2" + keystone_db_sync: "ghcr.io/rackerlabs/understack/keystone:2025.2" + keystone_domain_manage: "ghcr.io/rackerlabs/understack/keystone:2025.2" + keystone_fernet_rotate: "ghcr.io/rackerlabs/understack/keystone:2025.2" + keystone_fernet_setup: "ghcr.io/rackerlabs/understack/keystone:2025.2" # ironic - ironic_api: "ghcr.io/rackerlabs/understack/ironic:2026.1" - ironic_conductor: "ghcr.io/rackerlabs/understack/ironic:2026.1" - ironic_pxe: "ghcr.io/rackerlabs/understack/ironic:2026.1" - ironic_pxe_init: "ghcr.io/rackerlabs/understack/ironic:2026.1" + ironic_api: "ghcr.io/rackerlabs/understack/ironic:pr-1876" + ironic_conductor: "ghcr.io/rackerlabs/understack/ironic:pr-1876" + ironic_pxe: "ghcr.io/rackerlabs/understack/ironic:pr-1876" + ironic_pxe_init: "ghcr.io/rackerlabs/understack/ironic:pr-1876" ironic_pxe_http: "docker.io/nginx:1.29.8" - ironic_db_sync: "ghcr.io/rackerlabs/understack/ironic:2026.1" + ironic_db_sync: "ghcr.io/rackerlabs/understack/ironic:pr-1876" # these want curl which apparently is in the openstack-client image - ironic_manage_cleaning_network: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" - ironic_retrive_cleaning_network: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" - ironic_retrive_swift_config: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" + ironic_manage_cleaning_network: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" + ironic_retrive_cleaning_network: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" + ironic_retrive_swift_config: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" # neutron - neutron_db_sync: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_dhcp: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_l3: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_l2gw: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_linuxbridge_agent: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_metadata: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_ovn_metadata: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_openvswitch_agent: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_server: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_rpc_server: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_bagpipe_bgp: "ghcr.io/rackerlabs/understack/neutron:2026.1" - neutron_netns_cleanup_cron: "ghcr.io/rackerlabs/understack/neutron:2026.1" + neutron_db_sync: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_dhcp: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_l3: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_l2gw: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_linuxbridge_agent: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_metadata: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_ovn_metadata: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_openvswitch_agent: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_server: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_rpc_server: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_bagpipe_bgp: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_netns_cleanup_cron: "ghcr.io/rackerlabs/understack/neutron:pr-1876" # nova - nova_api: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_cell_setup: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_cell_setup_init: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" - nova_compute: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_compute_ironic: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_compute_ssh: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_conductor: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_db_sync: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_novncproxy: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_novncproxy_assets: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_scheduler: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_spiceproxy: "ghcr.io/rackerlabs/understack/nova:2026.1" - nova_spiceproxy_assets: "ghcr.io/rackerlabs/understack/nova:2026.1" + nova_api: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_cell_setup: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_cell_setup_init: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" + nova_compute: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_compute_ironic: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_compute_ssh: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_conductor: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_db_sync: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_novncproxy: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_novncproxy_assets: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_scheduler: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_spiceproxy: "ghcr.io/rackerlabs/understack/nova:pr-1876" + nova_spiceproxy_assets: "ghcr.io/rackerlabs/understack/nova:pr-1876" nova_service_cleaner: "docker.io/openstackhelm/ceph-config-helper:latest-ubuntu_jammy" # placement - placement: "ghcr.io/rackerlabs/understack/placement:2026.1" - placement_db_sync: "ghcr.io/rackerlabs/understack/placement:2026.1" + placement: "ghcr.io/rackerlabs/understack/placement:pr-1876" + placement_db_sync: "ghcr.io/rackerlabs/understack/placement:pr-1876" # openvswitch openvswitch_db_server: "docker.io/openstackhelm/openvswitch:ubuntu_jammy-dpdk-20250127" @@ -78,36 +78,36 @@ images: ovn_controller: "docker.io/openstackhelm/ovn:ubuntu_jammy-20250111" # horizon - horizon: "ghcr.io/rackerlabs/understack/horizon:2025.2" - horizon_db_sync: "ghcr.io/rackerlabs/understack/horizon:2025.2" + horizon: "ghcr.io/rackerlabs/understack/horizon:pr-1876" + horizon_db_sync: "ghcr.io/rackerlabs/understack/horizon:pr-1876" # glance - glance_api: "ghcr.io/rackerlabs/understack/glance:2026.1" - glance_db_sync: "ghcr.io/rackerlabs/understack/glance:2026.1" - glance_metadefs_load: "ghcr.io/rackerlabs/understack/glance:2026.1" + glance_api: "ghcr.io/rackerlabs/understack/glance:pr-1876" + glance_db_sync: "ghcr.io/rackerlabs/understack/glance:pr-1876" + glance_metadefs_load: "ghcr.io/rackerlabs/understack/glance:pr-1876" glance_storage_init: "docker.io/openstackhelm/ceph-config-helper:latest-ubuntu_jammy" # skyline - skyline: "ghcr.io/rackerlabs/understack/skyline:2025.2" - skyline_db_sync: "ghcr.io/rackerlabs/understack/skyline:2025.2" - skyline_nginx: "ghcr.io/rackerlabs/understack/skyline:2025.2" + skyline: "ghcr.io/rackerlabs/understack/skyline:pr-1876" + skyline_db_sync: "ghcr.io/rackerlabs/understack/skyline:pr-1876" + skyline_nginx: "ghcr.io/rackerlabs/understack/skyline:pr-1876" # cinder - cinder_api: "ghcr.io/rackerlabs/understack/cinder:2025.2" - cinder_db_sync: "ghcr.io/rackerlabs/understack/cinder:2025.2" - cinder_scheduler: "ghcr.io/rackerlabs/understack/cinder:2025.2" - cinder_volume: "ghcr.io/rackerlabs/understack/cinder:2025.2" - cinder_volume_usage_audit: "ghcr.io/rackerlabs/understack/cinder:2025.2" - cinder_db_purge: "ghcr.io/rackerlabs/understack/cinder:2025.2" - cinder_backup: "ghcr.io/rackerlabs/understack/cinder:2025.2" + cinder_api: "ghcr.io/rackerlabs/understack/cinder:pr-1876" + cinder_db_sync: "ghcr.io/rackerlabs/understack/cinder:pr-1876" + cinder_scheduler: "ghcr.io/rackerlabs/understack/cinder:pr-1876" + cinder_volume: "ghcr.io/rackerlabs/understack/cinder:pr-1876" + cinder_volume_usage_audit: "ghcr.io/rackerlabs/understack/cinder:pr-1876" + cinder_db_purge: "ghcr.io/rackerlabs/understack/cinder:pr-1876" + cinder_backup: "ghcr.io/rackerlabs/understack/cinder:pr-1876" cinder_storage_init: "docker.io/openstackhelm/ceph-config-helper:latest-ubuntu_jammy" cinder_backup_storage_init: "docker.io/openstackhelm/ceph-config-helper:latest-ubuntu_jammy" # octavia - octavia_api: "ghcr.io/rackerlabs/understack/octavia:2025.2" - octavia_db_sync: "ghcr.io/rackerlabs/understack/octavia:2025.2" - octavia_worker: "ghcr.io/rackerlabs/understack/octavia:2025.2" - octavia_housekeeping: "ghcr.io/rackerlabs/understack/octavia:2025.2" - octavia_health_manager: "ghcr.io/rackerlabs/understack/octavia:2025.2" - octavia_health_manager_init: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" + octavia_api: "ghcr.io/rackerlabs/understack/octavia:pr-1876" + octavia_db_sync: "ghcr.io/rackerlabs/understack/octavia:pr-1876" + octavia_worker: "ghcr.io/rackerlabs/understack/octavia:pr-1876" + octavia_housekeeping: "ghcr.io/rackerlabs/understack/octavia:pr-1876" + octavia_health_manager: "ghcr.io/rackerlabs/understack/octavia:pr-1876" + octavia_health_manager_init: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" ... From 2612000d0d9dddb1918f33337f2ba5375a7b9df3 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 14 May 2026 10:41:59 +0100 Subject: [PATCH 05/33] chore: update keystone-service-user secrets to OIDC --- .../templates/keystone-service-user.yaml.tpl | 93 ++++++------------- components/openstack/values.schema.json | 10 ++ components/openstack/values.yaml | 4 + 3 files changed, 43 insertions(+), 64 deletions(-) diff --git a/components/openstack/templates/keystone-service-user.yaml.tpl b/components/openstack/templates/keystone-service-user.yaml.tpl index b4b2e0a3b..489747e92 100644 --- a/components/openstack/templates/keystone-service-user.yaml.tpl +++ b/components/openstack/templates/keystone-service-user.yaml.tpl @@ -2,80 +2,45 @@ {{- range $serviceName, $users := .Values.keystoneServiceUsers.services }} {{- range $_, $user := $users }} --- -apiVersion: external-secrets.io/v1 -kind: ExternalSecret +apiVersion: v1 +kind: Secret metadata: name: {{ (printf "%s-keystone-%s" $serviceName $user.usage) | quote }} -{{- if $.Values.keystoneServiceUsers.externalLinkAnnotationTemplate }} - annotations: - link.argocd.argoproj.io/external-link: {{ tpl $.Values.keystoneServiceUsers.externalLinkAnnotationTemplate (dict "remoteRef" $user.remoteRef) | quote }} -{{- end }} -spec: - refreshInterval: {{ $.Values.externalSecretsRefreshInterval | default "1h" }} - secretStoreRef: - kind: {{ $.Values.keystoneServiceUsers.secretStore.kind | quote }} - name: {{ $.Values.keystoneServiceUsers.secretStore.name | quote }} - target: - name: {{ (printf "%s-keystone-%s" $serviceName $user.usage) | quote }} - template: - engineVersion: v2 - data: - OS_AUTH_URL: {{ $.Values.keystoneUrl | quote }} - OS_DEFAULT_DOMAIN: 'default' - OS_INTERFACE: {{ $.Values.keystoneServiceUsers.keystoneInterface | quote }} - OS_PROJECT_DOMAIN_NAME: {{ include "openstack.serviceuser.project_domain_name" $user | quote }} - OS_PROJECT_NAME: {{ include "openstack.serviceuser.project_name" $user | quote }} - OS_USER_DOMAIN_NAME: {{ include "openstack.serviceuser.user_domain_name" $user | quote }} - OS_USERNAME: {{ `{{ .username }}` | quote }} - OS_PASSWORD: {{ `{{ .password }}` | quote }} - OS_REGION_NAME: {{ $.Values.regionName | quote }} - dataFrom: - - extract: - key: {{ $user.remoteRef | quote }} +data: + OS_AUTH_TYPE: v3oidcaccesstokenfile + OS_AUTH_URL: {{ $.Values.keystoneUrl | quote }} + OS_DEFAULT_DOMAIN: "default" + OS_INTERFACE: {{ $.Values.keystoneServiceUsers.keystoneInterface | quote }} + OS_PROJECT_DOMAIN_NAME: {{ include "openstack.serviceuser.project_domain_name" $user | quote }} + OS_PROJECT_NAME: {{ include "openstack.serviceuser.project_name" $user | quote }} + OS_USER_DOMAIN_NAME: {{ include "openstack.serviceuser.user_domain_name" $user | quote }} + OS_REGION_NAME: {{ $.Values.regionName | quote }} + OS_IDENTITY_PROVIDER: {{ $.Values.keystoneServiceUsers.identityProvider }} + OS_PROTOCOL: openid + OS_ACCESS_TOKEN_FILE: {{ $.Values.keystoneServiceUsers.accessTokenFile }} {{- end }} {{- end }} {{- range $serviceName, $users := .Values.keystoneServiceUsers.services }} {{- if not (eq $serviceName "keystone") }} --- -apiVersion: external-secrets.io/v1 -kind: ExternalSecret +apiVersion: v1 +kind: Secret metadata: name: {{ (printf "%s-ks-etc" $serviceName) | quote }} -spec: - refreshInterval: {{ $.Values.externalSecretsRefreshInterval | default "1h" }} - secretStoreRef: - kind: {{ $.Values.keystoneServiceUsers.secretStore.kind | quote }} - name: {{ $.Values.keystoneServiceUsers.secretStore.name | quote }} - target: - name: {{ (printf "%s-ks-etc" $serviceName) | quote }} - template: - engineVersion: v2 - data: - {{ (printf "%s_auth.conf" $serviceName) | quote }}: | - {{- range $_, $user := $users }} - {{- $section := $user.section | default $user.usage }} - {{- $section := eq $section "user" | ternary "keystone_authtoken" $section -}} - {{- $shouldSkip := or (eq $user.usage "test") (eq $user.usage "admin") }} - {{- if not $shouldSkip }} - [{{ $section }}] - auth_url={{ $.Values.keystoneUrl }} - project_domain_name={{ include "openstack.serviceuser.project_domain_name" $user }} - project_name={{ include "openstack.serviceuser.project_name" $user }} - user_domain_name={{ include "openstack.serviceuser.user_domain_name" $user }} - username={{ printf "{{ (fromJson .%s).username }}" $user.usage }} - password={{ printf "{{ (fromJson .%s).password }}" $user.usage }} - region_name={{ $.Values.regionName }} - {{- end }} - {{- end }} - data: - {{/* default section name is the usage field */}} - {{/* usage test and admin are special in OpenStack Helm and not added to the config file */}} - {{- range $_, $user := $users -}} - {{- $shouldSkip := or (eq $user.usage "test") (eq $user.usage "admin") -}} +data: + {{ (printf "%s_auth.conf" $serviceName) | quote }}: | + {{- range $_, $user := $users }} + {{- $section := $user.section | default $user.usage }} + {{- $section := eq $section "user" | ternary "keystone_authtoken" $section -}} + {{- $shouldSkip := or (eq $user.usage "test") (eq $user.usage "admin") }} {{- if not $shouldSkip }} - - secretKey: {{ $user.usage }} - remoteRef: - key: {{ $user.remoteRef | quote }} + [{{ $section }}] + auth_type=v3oidcaccesstokenfile + auth_url={{ $.Values.keystoneUrl }} + identity_provider={{ $.Values.identityProvider }} + protocol=openid + access_token_file=/var/run/secrets/kubernetes.io/serviceaccount/token + region_name={{ $.Values.regionName }} {{- end }} {{- end }} {{- end }} diff --git a/components/openstack/values.schema.json b/components/openstack/values.schema.json index 5f0025ef2..0dea6f9ad 100644 --- a/components/openstack/values.schema.json +++ b/components/openstack/values.schema.json @@ -317,6 +317,16 @@ "default": "internal", "description": "OpenStack service catalog interface to use" }, + "keystoneIdentityProvider": { + "type": "string", + "default": "undefined", + "description": "Keystone Identity provider name" + }, + "accessTokenFile": { + "type": "string", + "default": "/var/run/secrets/kubernetes.io/serviceaccount/token", + "description": "Path to a file with Kubernetes token" + }, "services": { "type": "object", "description": "Service users organized by service name", diff --git a/components/openstack/values.yaml b/components/openstack/values.yaml index 809c33941..fced7aca2 100644 --- a/components/openstack/values.yaml +++ b/components/openstack/values.yaml @@ -83,6 +83,10 @@ keystoneServiceUsers: externalLinkAnnotationTemplate: "" # -- OpenStack service catalog interface to use keystoneInterface: "internal" + # -- Identity provider name (as created in Keystone) + keystoneIdentityProvider: "k8s-site-a" + # -- Path to Kubernetes service account token file + accessTokenFile: "/var/run/secrets/kubernetes.io/serviceaccount/token" # -- OpenStack services with a definition of each of their service accounts services: keystone: From 4c67915bf2089e9c57aeccddb03f5cb20bd37414 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 14 May 2026 11:40:46 +0100 Subject: [PATCH 06/33] chore: generate provider metadata script --- components/keystone/values.yaml | 71 +++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/components/keystone/values.yaml b/components/keystone/values.yaml index 1eff67a9e..c16626116 100644 --- a/components/keystone/values.yaml +++ b/components/keystone/values.yaml @@ -134,6 +134,16 @@ pod: timeoutSeconds: 15 mounts: keystone_api: + init_container: + - name: custom-config-generator + image: ghcr.io/rackerlabs/understack/keystone:2025.2 + command: ["python3", "/scripts/generate_configs.py"] + volumeMounts: + - name: oidc-gen-providers + mountPath: /scripts + readOnly: true + - name: oidc-gen-output + mountPath: /srv/generated keystone_api: volumeMounts: - name: keystone-sso @@ -142,6 +152,11 @@ pod: - name: oidc-secret mountPath: /etc/oidc-secret readOnly: true + - name: oidc-gen-providers + mountPath: /scripts + readOnly: true + - name: oidc-gen-output + mountPath: /etc/mod_auth_openidc/metadata volumes: - name: keystone-sso secret: @@ -149,6 +164,12 @@ pod: - name: oidc-secret secret: secretName: sso-passphrase + - name: oidc-gen-output + configMap: + name: keystone-gen-oidc-metadata + defaultMode: 0555 + - name: oidc-gen-providers + emptyDir: {} keystone_bootstrap: keystone_bootstrap: volumeMounts: @@ -402,3 +423,53 @@ annotations: # relies on services to be up so it can remain post argocd.argoproj.io/hook-delete-policy: BeforeHookCreation argocd.argoproj.io/sync-options: Force=true +extraObjects: + - apiVersion: v1 + kind: ConfigMap + metadata: + name: keystone-gen-oidc-metadata + namespace: openstack + data: + generate_configs.py: | + #!/usr/bin/env python3 + + import json + import ssl + import urllib.parse + import urllib.request + from pathlib import Path + + METADATA_DIR = Path("/srv/generated") + CLIENT_ID = "https://kubernetes.default.svc.cluster.local" + + CLUSTERS = { + "rax-dev-iad3-dev": "https://uc-iad.dev.undercloud.rackspace.net:6443", + "rax-dev-global": "https://uc-dev-global.k8s-api.pvceng.rax.io:443", + } + + METADATA_DIR.mkdir(parents=True, exist_ok=True) + + # Skip TLS verification — replace with a real SSLContext if you have the CA bundles + ctx = ssl._create_unverified_context() + + for site, issuer in CLUSTERS.items(): + encoded = urllib.parse.quote(issuer, safe="") + base = METADATA_DIR / encoded + + print(f"Generating metadata for {site} ({issuer})") + + discovery_url = f"{issuer}/.well-known/openid-configuration" + with urllib.request.urlopen(discovery_url, context=ctx) as resp: + provider_metadata = json.load(resp) + + (base.with_suffix(".provider")).write_text(json.dumps(provider_metadata, indent=2)) + + (base.with_suffix(".client")).write_text(json.dumps({ + "client_id": CLIENT_ID, + "client_secret": "not-used", + }, indent=2)) + + (base.with_suffix(".conf")).write_text(json.dumps({ + "response_type": "id_token", + "scope": "openid", + }, indent=2)) From b98e8f627eba32dcf31b068ff9fc5aea35eba420 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 14 May 2026 12:04:55 +0100 Subject: [PATCH 07/33] chore: recreate keystne-keystone-admin from ES --- .../templates/keystone-service-user.yaml.tpl | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/components/openstack/templates/keystone-service-user.yaml.tpl b/components/openstack/templates/keystone-service-user.yaml.tpl index 489747e92..855da7f85 100644 --- a/components/openstack/templates/keystone-service-user.yaml.tpl +++ b/components/openstack/templates/keystone-service-user.yaml.tpl @@ -46,3 +46,42 @@ data: {{- end }} {{- end }} {{- end }} +--- +# Keystone admin must still be placed in ES +{{- range $serviceName, $users := .Values.keystoneServiceUsers.services }} +{{- range $_, $user := $users }} + +{{- if eq $user "keystone" }} +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: keystone-keystone-admin +{{- if $.Values.keystoneServiceUsers.externalLinkAnnotationTemplate }} + annotations: + link.argocd.argoproj.io/external-link: {{ tpl $.Values.keystoneServiceUsers.externalLinkAnnotationTemplate (dict "remoteRef" $user.remoteRef) | quote }} +{{- end }} +spec: + refreshInterval: {{ $.Values.externalSecretsRefreshInterval | default "1h" }} + secretStoreRef: + kind: {{ $.Values.keystoneServiceUsers.secretStore.kind | quote }} + name: {{ $.Values.keystoneServiceUsers.secretStore.name | quote }} + target: + name: {{ (printf "%s-keystone-%s" $serviceName $user.usage) | quote }} + template: + engineVersion: v2 + data: + OS_AUTH_URL: {{ $.Values.keystoneUrl | quote }} + OS_DEFAULT_DOMAIN: 'default' + OS_INTERFACE: {{ $.Values.keystoneServiceUsers.keystoneInterface | quote }} + OS_PROJECT_DOMAIN_NAME: {{ include "openstack.serviceuser.project_domain_name" $user | quote }} + OS_PROJECT_NAME: {{ include "openstack.serviceuser.project_name" $user | quote }} + OS_USER_DOMAIN_NAME: {{ include "openstack.serviceuser.user_domain_name" $user | quote }} + OS_USERNAME: {{ `{{ .username }}` | quote }} + OS_PASSWORD: {{ `{{ .password }}` | quote }} + OS_REGION_NAME: {{ $.Values.regionName | quote }} + dataFrom: + - extract: + key: {{ $user.remoteRef | quote }} +{{- end }} +{{- end }} +{{- end }} From 2e96d42428324db6d3137cf68e47acca3cffcc88 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 14 May 2026 12:41:12 +0100 Subject: [PATCH 08/33] fix: try adding volume to init_container --- components/keystone/values.yaml | 18 +++++++++--------- .../templates/keystone-service-user.yaml.tpl | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/components/keystone/values.yaml b/components/keystone/values.yaml index c16626116..2b4dbe9bb 100644 --- a/components/keystone/values.yaml +++ b/components/keystone/values.yaml @@ -135,15 +135,15 @@ pod: mounts: keystone_api: init_container: - - name: custom-config-generator - image: ghcr.io/rackerlabs/understack/keystone:2025.2 - command: ["python3", "/scripts/generate_configs.py"] - volumeMounts: - - name: oidc-gen-providers - mountPath: /scripts - readOnly: true - - name: oidc-gen-output - mountPath: /srv/generated + - name: oidc-gen-providers + mountPath: /scripts + readOnly: true + - name: oidc-gen-output + mountPath: /srv/generated + # - name: custom-config-generator + # image: ghcr.io/rackerlabs/understack/keystone:2025.2 + # command: ["python3", "/scripts/generate_configs.py"] + # volumeMounts: keystone_api: volumeMounts: - name: keystone-sso diff --git a/components/openstack/templates/keystone-service-user.yaml.tpl b/components/openstack/templates/keystone-service-user.yaml.tpl index 855da7f85..6dac274ef 100644 --- a/components/openstack/templates/keystone-service-user.yaml.tpl +++ b/components/openstack/templates/keystone-service-user.yaml.tpl @@ -51,7 +51,7 @@ data: {{- range $serviceName, $users := .Values.keystoneServiceUsers.services }} {{- range $_, $user := $users }} -{{- if eq $user "keystone" }} +{{- if eq $serviceName "keystone" }} apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: From 19b99dc624c920d1e1e3a27941dff81e6ef4ad7d Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Fri, 15 May 2026 10:33:17 +0100 Subject: [PATCH 09/33] oidc: change approach from init container to static CM --- components/keystone/values.yaml | 68 +++------------------------------ 1 file changed, 6 insertions(+), 62 deletions(-) diff --git a/components/keystone/values.yaml b/components/keystone/values.yaml index 2b4dbe9bb..d1df1abbe 100644 --- a/components/keystone/values.yaml +++ b/components/keystone/values.yaml @@ -134,16 +134,6 @@ pod: timeoutSeconds: 15 mounts: keystone_api: - init_container: - - name: oidc-gen-providers - mountPath: /scripts - readOnly: true - - name: oidc-gen-output - mountPath: /srv/generated - # - name: custom-config-generator - # image: ghcr.io/rackerlabs/understack/keystone:2025.2 - # command: ["python3", "/scripts/generate_configs.py"] - # volumeMounts: keystone_api: volumeMounts: - name: keystone-sso @@ -152,11 +142,8 @@ pod: - name: oidc-secret mountPath: /etc/oidc-secret readOnly: true - - name: oidc-gen-providers - mountPath: /scripts + - name: oidc-metadata readOnly: true - - name: oidc-gen-output - mountPath: /etc/mod_auth_openidc/metadata volumes: - name: keystone-sso secret: @@ -164,12 +151,10 @@ pod: - name: oidc-secret secret: secretName: sso-passphrase - - name: oidc-gen-output + - name: oidc-metadata configMap: - name: keystone-gen-oidc-metadata + name: keystone-oidc-metadata defaultMode: 0555 - - name: oidc-gen-providers - emptyDir: {} keystone_bootstrap: keystone_bootstrap: volumeMounts: @@ -423,53 +408,12 @@ annotations: # relies on services to be up so it can remain post argocd.argoproj.io/hook-delete-policy: BeforeHookCreation argocd.argoproj.io/sync-options: Force=true + extraObjects: - apiVersion: v1 kind: ConfigMap metadata: - name: keystone-gen-oidc-metadata + name: keystone-oidc-metadata namespace: openstack data: - generate_configs.py: | - #!/usr/bin/env python3 - - import json - import ssl - import urllib.parse - import urllib.request - from pathlib import Path - - METADATA_DIR = Path("/srv/generated") - CLIENT_ID = "https://kubernetes.default.svc.cluster.local" - - CLUSTERS = { - "rax-dev-iad3-dev": "https://uc-iad.dev.undercloud.rackspace.net:6443", - "rax-dev-global": "https://uc-dev-global.k8s-api.pvceng.rax.io:443", - } - - METADATA_DIR.mkdir(parents=True, exist_ok=True) - - # Skip TLS verification — replace with a real SSLContext if you have the CA bundles - ctx = ssl._create_unverified_context() - - for site, issuer in CLUSTERS.items(): - encoded = urllib.parse.quote(issuer, safe="") - base = METADATA_DIR / encoded - - print(f"Generating metadata for {site} ({issuer})") - - discovery_url = f"{issuer}/.well-known/openid-configuration" - with urllib.request.urlopen(discovery_url, context=ctx) as resp: - provider_metadata = json.load(resp) - - (base.with_suffix(".provider")).write_text(json.dumps(provider_metadata, indent=2)) - - (base.with_suffix(".client")).write_text(json.dumps({ - "client_id": CLIENT_ID, - "client_secret": "not-used", - }, indent=2)) - - (base.with_suffix(".conf")).write_text(json.dumps({ - "response_type": "id_token", - "scope": "openid", - }, indent=2)) + # Define your OIDC providers metadata in deploy repo From 7eec5453599af92255c38fc206da00a56245f528 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 20 May 2026 16:45:27 +0100 Subject: [PATCH 10/33] keystone: add mod_oauth2 module --- containers/keystone/Dockerfile | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/containers/keystone/Dockerfile b/containers/keystone/Dockerfile index dd9e62d5c..05c3816da 100644 --- a/containers/keystone/Dockerfile +++ b/containers/keystone/Dockerfile @@ -8,7 +8,22 @@ ARG MOD_AUTH_OPENIDC_VERSION=2.4.16.11 ARG MOD_AUTH_OPENIDC_SHA256=431c4ffb0e26d59ef0e60afa0fdeab61466faeecafdd237355fdcccf7858b37f ADD --checksum=sha256:${MOD_AUTH_OPENIDC_SHA256} https://github.com/OpenIDC/mod_auth_openidc/releases/download/v${MOD_AUTH_OPENIDC_VERSION}/libapache2-mod-auth-openidc_${MOD_AUTH_OPENIDC_VERSION}-1.noble_amd64.deb /tmp +# renovate: datasource=github-releases depName=OpenIDC/liboauth2 +ARG LIBOAUTH2_VERSION=2.2.1 +ARG LIBOAUTH2_SHA256=a971d00469b27679c49d4f0e792261512125598a063aea4593a0fe174188acea +ARG LIBOAUTH2_APACHE_SHA256=a6dcb37bbbde47862aa298df71b63ca5237ceb9ebf88faa1598157994fc30855 +ADD --checksum=sha256:${LIBOAUTH2_SHA256} https://github.com/OpenIDC/liboauth2/releases/download/v${LIBOAUTH2_VERSION}/liboauth2_${LIBOAUTH2_VERSION}-1.noble_amd64.deb /tmp +ADD --checksum=sha256:${LIBOAUTH2_APACHE_SHA256} https://github.com/OpenIDC/liboauth2/releases/download/v${LIBOAUTH2_VERSION}/liboauth2-apache_${LIBOAUTH2_VERSION}-1.noble_amd64.deb /tmp + +# renovate: datasource=github-releases depName=OpenIDC/mod_oauth2 +ARG MOD_OAUTH2_VERSION=4.1.0 +ARG MOD_OAUTH2_SHA256=717bf50c7b7c6e25375d350ec5c0fef4e1971a97ffc5efc5732f52d38bceb07a +ADD --checksum=sha256:${MOD_OAUTH2_SHA256} https://github.com/OpenIDC/mod_oauth2/releases/download/v${MOD_OAUTH2_VERSION}/libapache2-mod-oauth2_${MOD_OAUTH2_VERSION}-1.noble_amd64.deb /tmp + RUN apt-get update && \ apt-get install -y --no-install-recommends \ /tmp/libapache2-mod-auth-openidc_${MOD_AUTH_OPENIDC_VERSION}-1.noble_amd64.deb \ + /tmp/liboauth2_${LIBOAUTH2_VERSION}-1.noble_amd64.deb \ + /tmp/liboauth2-apache_${LIBOAUTH2_VERSION}-1.noble_amd64.deb \ + /tmp/libapache2-mod-oauth2_${MOD_OAUTH2_VERSION}-1.noble_amd64.deb \ && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/*.deb From 99723cfeda452d85a52cdc06678efb88e6cacecf Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 20 May 2026 17:10:27 +0100 Subject: [PATCH 11/33] keystone: enable oauth2 module on build --- containers/keystone/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/containers/keystone/Dockerfile b/containers/keystone/Dockerfile index 05c3816da..f7f4a1fd0 100644 --- a/containers/keystone/Dockerfile +++ b/containers/keystone/Dockerfile @@ -27,3 +27,5 @@ RUN apt-get update && \ /tmp/liboauth2-apache_${LIBOAUTH2_VERSION}-1.noble_amd64.deb \ /tmp/libapache2-mod-oauth2_${MOD_OAUTH2_VERSION}-1.noble_amd64.deb \ && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/*.deb + +RUN a2enmod oauth2 From e377b7c6e0f2f1bd301272bb66407583cd3d42e6 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 20 May 2026 17:15:19 +0100 Subject: [PATCH 12/33] keystone: try oauth2 with ld cache refresh --- containers/keystone/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/containers/keystone/Dockerfile b/containers/keystone/Dockerfile index f7f4a1fd0..9dd4ff1b7 100644 --- a/containers/keystone/Dockerfile +++ b/containers/keystone/Dockerfile @@ -26,6 +26,7 @@ RUN apt-get update && \ /tmp/liboauth2_${LIBOAUTH2_VERSION}-1.noble_amd64.deb \ /tmp/liboauth2-apache_${LIBOAUTH2_VERSION}-1.noble_amd64.deb \ /tmp/libapache2-mod-oauth2_${MOD_OAUTH2_VERSION}-1.noble_amd64.deb \ - && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/*.deb + && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/*.deb \ + && ldconfig RUN a2enmod oauth2 From 72509942bf80d4c976eba860231060eeba33222d Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 20 May 2026 17:21:32 +0100 Subject: [PATCH 13/33] keystone: switch to libapache2-mod-oauth2 provided by Ubuntu --- containers/keystone/Dockerfile | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/containers/keystone/Dockerfile b/containers/keystone/Dockerfile index 9dd4ff1b7..66df8e795 100644 --- a/containers/keystone/Dockerfile +++ b/containers/keystone/Dockerfile @@ -8,25 +8,10 @@ ARG MOD_AUTH_OPENIDC_VERSION=2.4.16.11 ARG MOD_AUTH_OPENIDC_SHA256=431c4ffb0e26d59ef0e60afa0fdeab61466faeecafdd237355fdcccf7858b37f ADD --checksum=sha256:${MOD_AUTH_OPENIDC_SHA256} https://github.com/OpenIDC/mod_auth_openidc/releases/download/v${MOD_AUTH_OPENIDC_VERSION}/libapache2-mod-auth-openidc_${MOD_AUTH_OPENIDC_VERSION}-1.noble_amd64.deb /tmp -# renovate: datasource=github-releases depName=OpenIDC/liboauth2 -ARG LIBOAUTH2_VERSION=2.2.1 -ARG LIBOAUTH2_SHA256=a971d00469b27679c49d4f0e792261512125598a063aea4593a0fe174188acea -ARG LIBOAUTH2_APACHE_SHA256=a6dcb37bbbde47862aa298df71b63ca5237ceb9ebf88faa1598157994fc30855 -ADD --checksum=sha256:${LIBOAUTH2_SHA256} https://github.com/OpenIDC/liboauth2/releases/download/v${LIBOAUTH2_VERSION}/liboauth2_${LIBOAUTH2_VERSION}-1.noble_amd64.deb /tmp -ADD --checksum=sha256:${LIBOAUTH2_APACHE_SHA256} https://github.com/OpenIDC/liboauth2/releases/download/v${LIBOAUTH2_VERSION}/liboauth2-apache_${LIBOAUTH2_VERSION}-1.noble_amd64.deb /tmp - -# renovate: datasource=github-releases depName=OpenIDC/mod_oauth2 -ARG MOD_OAUTH2_VERSION=4.1.0 -ARG MOD_OAUTH2_SHA256=717bf50c7b7c6e25375d350ec5c0fef4e1971a97ffc5efc5732f52d38bceb07a -ADD --checksum=sha256:${MOD_OAUTH2_SHA256} https://github.com/OpenIDC/mod_oauth2/releases/download/v${MOD_OAUTH2_VERSION}/libapache2-mod-oauth2_${MOD_OAUTH2_VERSION}-1.noble_amd64.deb /tmp - RUN apt-get update && \ apt-get install -y --no-install-recommends \ /tmp/libapache2-mod-auth-openidc_${MOD_AUTH_OPENIDC_VERSION}-1.noble_amd64.deb \ - /tmp/liboauth2_${LIBOAUTH2_VERSION}-1.noble_amd64.deb \ - /tmp/liboauth2-apache_${LIBOAUTH2_VERSION}-1.noble_amd64.deb \ - /tmp/libapache2-mod-oauth2_${MOD_OAUTH2_VERSION}-1.noble_amd64.deb \ - && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/*.deb \ - && ldconfig + libapache2-mod-oauth2 \ + && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/*.deb RUN a2enmod oauth2 From 9870cf899d93a3c5c26d69492803f9b145e19156 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 26 May 2026 10:22:32 +0100 Subject: [PATCH 14/33] keystone: create k8s service account group and roles --- .../keystone_bootstrap/defaults/main.yml | 19 +++++++++++ .../roles/keystone_bootstrap/tasks/k8s.yml | 20 +++++++++++ .../keystone_bootstrap/tasks/k8s_group.yml | 33 +++++++++++++++++++ .../roles/keystone_bootstrap/tasks/main.yml | 3 ++ 4 files changed, 75 insertions(+) create mode 100644 ansible/roles/keystone_bootstrap/tasks/k8s.yml create mode 100644 ansible/roles/keystone_bootstrap/tasks/k8s_group.yml diff --git a/ansible/roles/keystone_bootstrap/defaults/main.yml b/ansible/roles/keystone_bootstrap/defaults/main.yml index 4b68e7e3e..c95b2d6de 100644 --- a/ansible/roles/keystone_bootstrap/defaults/main.yml +++ b/ansible/roles/keystone_bootstrap/defaults/main.yml @@ -68,3 +68,22 @@ keystone_bootstrap_groups: # role: member # - project: shared-services # role: reader + +# k8s service account groups and their project role assignments +# Each group can have project_roles with an optional domain to scope the project lookup +keystone_bootstrap_k8s_groups: + - name: k8s-serviceaccounts + desc: 'Kubernetes Service Accounts' + project_roles: + - project: service + domain: service + role: admin + - project: service + domain: service + role: service + - project: baremetal + domain: infra + role: admin + - project: baremetal + domain: infra + role: service diff --git a/ansible/roles/keystone_bootstrap/tasks/k8s.yml b/ansible/roles/keystone_bootstrap/tasks/k8s.yml new file mode 100644 index 000000000..96df1f2db --- /dev/null +++ b/ansible/roles/keystone_bootstrap/tasks/k8s.yml @@ -0,0 +1,20 @@ +--- +# Copyright (c) 2026 Rackspace Technology, Inc. +# +# 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. + +- name: Create k8s service account group mappings + ansible.builtin.include_tasks: k8s_group.yml + loop: "{{ keystone_bootstrap_k8s_groups }}" + loop_control: + loop_var: group_item diff --git a/ansible/roles/keystone_bootstrap/tasks/k8s_group.yml b/ansible/roles/keystone_bootstrap/tasks/k8s_group.yml new file mode 100644 index 000000000..ab0587f2a --- /dev/null +++ b/ansible/roles/keystone_bootstrap/tasks/k8s_group.yml @@ -0,0 +1,33 @@ +--- +# Copyright (c) 2026 Rackspace Technology, Inc. +# +# 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. + +- name: Create k8s group + openstack.cloud.identity_group: + name: "{{ group_item.name }}" + description: "{{ group_item.desc }}" + state: present + register: _k8s_group + +- name: Assign role to k8s group for project + openstack.cloud.role_assignment: + group: "{{ _k8s_group.group.id }}" + project: "{{ role_item.project }}" + project_domain: "{{ role_item.domain | default(omit) }}" + role: "{{ role_item.role }}" + state: present + loop: "{{ group_item.project_roles | default([]) }}" + loop_control: + loop_var: role_item + when: dont_set_roles is not defined diff --git a/ansible/roles/keystone_bootstrap/tasks/main.yml b/ansible/roles/keystone_bootstrap/tasks/main.yml index 82c1caf8f..4365796fc 100644 --- a/ansible/roles/keystone_bootstrap/tasks/main.yml +++ b/ansible/roles/keystone_bootstrap/tasks/main.yml @@ -10,5 +10,8 @@ - name: Define SSO ansible.builtin.include_tasks: sso.yml +- name: Define k8s service account groups + ansible.builtin.include_tasks: k8s.yml + - name: Define misc keystone ansible.builtin.include_tasks: misc.yml From 141aeb59b5fe7fb7e29c85f462b912a3abedc5ed Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 26 May 2026 10:53:08 +0100 Subject: [PATCH 15/33] feat: create federation mapping for k8s --- ansible/roles/keystone_bootstrap/tasks/k8s.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ansible/roles/keystone_bootstrap/tasks/k8s.yml b/ansible/roles/keystone_bootstrap/tasks/k8s.yml index 96df1f2db..ba82f13e1 100644 --- a/ansible/roles/keystone_bootstrap/tasks/k8s.yml +++ b/ansible/roles/keystone_bootstrap/tasks/k8s.yml @@ -13,6 +13,23 @@ # License for the specific language governing permissions and limitations # under the License. +- name: Create k8s mapping + openstack.cloud.federation_mapping: + name: k8s-mapping + rules: + - local: + - user: + name: '{0}' + group: + domain: + name: service + name: k8s-serviceaccounts + remote: + - type: HTTP_OIDC_SUB + - type: HTTP_OIDC_AUD + any_one_of: + - keystone + - name: Create k8s service account group mappings ansible.builtin.include_tasks: k8s_group.yml loop: "{{ keystone_bootstrap_k8s_groups }}" From d86bb14b17345552da41f62ac4a0ae066d078f68 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 26 May 2026 11:26:39 +0100 Subject: [PATCH 16/33] keystone: create k8s identity providers and protocols --- .../roles/keystone_bootstrap/defaults/main.yml | 4 ++++ ansible/roles/keystone_bootstrap/tasks/k8s.yml | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/ansible/roles/keystone_bootstrap/defaults/main.yml b/ansible/roles/keystone_bootstrap/defaults/main.yml index c95b2d6de..1a4a50a71 100644 --- a/ansible/roles/keystone_bootstrap/defaults/main.yml +++ b/ansible/roles/keystone_bootstrap/defaults/main.yml @@ -69,6 +69,10 @@ keystone_bootstrap_groups: # - project: shared-services # role: reader +# k8s identity providers; override with your cluster issuers +keystone_k8s_identity: + providers: [] + # k8s service account groups and their project role assignments # Each group can have project_roles with an optional domain to scope the project lookup keystone_bootstrap_k8s_groups: diff --git a/ansible/roles/keystone_bootstrap/tasks/k8s.yml b/ansible/roles/keystone_bootstrap/tasks/k8s.yml index ba82f13e1..173b408a5 100644 --- a/ansible/roles/keystone_bootstrap/tasks/k8s.yml +++ b/ansible/roles/keystone_bootstrap/tasks/k8s.yml @@ -30,6 +30,21 @@ any_one_of: - keystone +- name: Create k8s identity providers + openstack.cloud.federation_idp: + name: "{{ item.name }}" + is_enabled: true + remote_ids: + - "{{ item.issuer }}" + loop: "{{ keystone_k8s_identity.providers }}" + +- name: Create k8s openid protocols + openstack.cloud.keystone_federation_protocol: + name: openid + idp: "{{ item.name }}" + mapping: k8s-mapping + loop: "{{ keystone_k8s_identity.providers }}" + - name: Create k8s service account group mappings ansible.builtin.include_tasks: k8s_group.yml loop: "{{ keystone_bootstrap_k8s_groups }}" From 02ab6c543331932639f3b68e7d91e6a809caed72 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 26 May 2026 13:31:55 +0100 Subject: [PATCH 17/33] keystone: stop requiring keystone aud in mapping We do enforcement on the apache/mod_oauth2 level instead. --- ansible/roles/keystone_bootstrap/tasks/k8s.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/ansible/roles/keystone_bootstrap/tasks/k8s.yml b/ansible/roles/keystone_bootstrap/tasks/k8s.yml index 173b408a5..865bf6685 100644 --- a/ansible/roles/keystone_bootstrap/tasks/k8s.yml +++ b/ansible/roles/keystone_bootstrap/tasks/k8s.yml @@ -26,9 +26,6 @@ name: k8s-serviceaccounts remote: - type: HTTP_OIDC_SUB - - type: HTTP_OIDC_AUD - any_one_of: - - keystone - name: Create k8s identity providers openstack.cloud.federation_idp: From c9ed5c843094120dea92df27dd361b6a82666397 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 26 May 2026 13:45:47 +0100 Subject: [PATCH 18/33] keystone: setup k8s-serviceaccounts in correct domain Without domain ID provided, it was defaulting to 'Default' rather than a 'service'. --- ansible/roles/keystone_bootstrap/defaults/main.yml | 3 +++ ansible/roles/keystone_bootstrap/tasks/k8s.yml | 5 +++++ ansible/roles/keystone_bootstrap/tasks/k8s_group.yml | 1 + 3 files changed, 9 insertions(+) diff --git a/ansible/roles/keystone_bootstrap/defaults/main.yml b/ansible/roles/keystone_bootstrap/defaults/main.yml index 1a4a50a71..b05784e31 100644 --- a/ansible/roles/keystone_bootstrap/defaults/main.yml +++ b/ansible/roles/keystone_bootstrap/defaults/main.yml @@ -69,6 +69,9 @@ keystone_bootstrap_groups: # - project: shared-services # role: reader +# domain in which k8s service account groups are created +keystone_bootstrap_k8s_group_domain: service + # k8s identity providers; override with your cluster issuers keystone_k8s_identity: providers: [] diff --git a/ansible/roles/keystone_bootstrap/tasks/k8s.yml b/ansible/roles/keystone_bootstrap/tasks/k8s.yml index 865bf6685..a8f05671a 100644 --- a/ansible/roles/keystone_bootstrap/tasks/k8s.yml +++ b/ansible/roles/keystone_bootstrap/tasks/k8s.yml @@ -42,6 +42,11 @@ mapping: k8s-mapping loop: "{{ keystone_k8s_identity.providers }}" +- name: Get k8s group domain info + openstack.cloud.identity_domain_info: + name: "{{ keystone_bootstrap_k8s_group_domain }}" + register: _k8s_group_domain + - name: Create k8s service account group mappings ansible.builtin.include_tasks: k8s_group.yml loop: "{{ keystone_bootstrap_k8s_groups }}" diff --git a/ansible/roles/keystone_bootstrap/tasks/k8s_group.yml b/ansible/roles/keystone_bootstrap/tasks/k8s_group.yml index ab0587f2a..7049950ef 100644 --- a/ansible/roles/keystone_bootstrap/tasks/k8s_group.yml +++ b/ansible/roles/keystone_bootstrap/tasks/k8s_group.yml @@ -16,6 +16,7 @@ - name: Create k8s group openstack.cloud.identity_group: name: "{{ group_item.name }}" + domain_id: "{{ _k8s_group_domain.domains[0].id }}" description: "{{ group_item.desc }}" state: present register: _k8s_group From 8a6c03aa48bb34ad0b62ceed3e8db43349bb6e2a Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 3 Jun 2026 09:22:08 +0100 Subject: [PATCH 19/33] chore: upgrade openstack to 2026.1 --- .github/workflows/containers-openstack.yaml | 47 +-------------------- charts/argocd-understack/values.yaml | 10 ++--- 2 files changed, 6 insertions(+), 51 deletions(-) diff --git a/.github/workflows/containers-openstack.yaml b/.github/workflows/containers-openstack.yaml index d31689eef..fc56555ec 100644 --- a/.github/workflows/containers-openstack.yaml +++ b/.github/workflows/containers-openstack.yaml @@ -36,6 +36,7 @@ jobs: - cinder - glance - horizon + - neutron - octavia - openstack-client - skyline @@ -44,51 +45,5 @@ jobs: with: container_name: ${{ matrix.project }} dockerfile_path: containers/${{ matrix.project }}/Dockerfile - build_args: OPENSTACK_VERSION=2025.2 - latest_name: "2025.2" - - placement: - uses: ./.github/workflows/build-container-reuse.yaml - secrets: inherit - with: - container_name: placement - dockerfile_path: containers/placement/Dockerfile - build_args: OPENSTACK_VERSION=2026.1 - latest_name: "2026.1" - - nova: - uses: ./.github/workflows/build-container-reuse.yaml - secrets: inherit - with: - container_name: nova - dockerfile_path: containers/nova/Dockerfile - build_args: OPENSTACK_VERSION=2026.1 - latest_name: "2026.1" - - neutron: - uses: ./.github/workflows/build-container-reuse.yaml - secrets: inherit - with: - container_name: neutron - dockerfile_path: containers/neutron/Dockerfile - build_args: OPENSTACK_VERSION=2026.1 - latest_name: "2026.1" - - ironic: - uses: ./.github/workflows/build-container-reuse.yaml - secrets: inherit - with: - container_name: ironic - dockerfile_path: containers/ironic/Dockerfile - build_args: OPENSTACK_VERSION=2026.1 - latest_name: "2026.1" - target: final - - keystone: - uses: ./.github/workflows/build-container-reuse.yaml - secrets: inherit - with: - container_name: keystone - dockerfile_path: containers/keystone/Dockerfile build_args: OPENSTACK_VERSION=2026.1 latest_name: "2026.1" diff --git a/charts/argocd-understack/values.yaml b/charts/argocd-understack/values.yaml index 956a50d80..53a9c84d9 100644 --- a/charts/argocd-understack/values.yaml +++ b/charts/argocd-understack/values.yaml @@ -371,7 +371,7 @@ site: wave: 2 # -- Chart version for Cinder # renovate: datasource=helm depName=cinder registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2025.1.3+abd55b4a7 + chartVersion: 2026.1.5+872fd69e7 # -- Ironic (Bare Metal Service) ironic: @@ -383,7 +383,7 @@ site: wave: 2 # -- Chart version for Ironic # renovate: datasource=helm depName=ironic registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2025.2.23+ea0d1ecda + chartVersion: 2026.1.7+872fd69e7 # -- Neutron (Networking Service) neutron: @@ -431,7 +431,7 @@ site: wave: 3 # -- Chart version for Octavia # renovate: datasource=helm depName=octavia registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2025.2.8+01c93d867 + chartVersion: 2026.1.4+872fd69e7 # -- Horizon (Dashboard) horizon: @@ -443,7 +443,7 @@ site: wave: 4 # -- Chart version for Horizon # renovate: datasource=helm depName=horizon registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2025.2.4+01c93d867 + chartVersion: 2026.1.2+872fd69e7 # -- Skyline (Dashboard) skyline: @@ -455,7 +455,7 @@ site: wave: 4 # -- Chart version for Skyline # renovate: datasource=helm depName=skyline registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2025.2.5+01c93d867 + chartVersion: 2026.1.2+872fd69e7 # -- Open vSwitch (Networking) openvswitch: From bfb252e6814371fbf9d2ea3b957dc500a52d4c6d Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 8 Jun 2026 16:46:45 +0100 Subject: [PATCH 20/33] include v3oidcaccessfiletoken in openstack-client container --- containers/openstack-client/Dockerfile | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/containers/openstack-client/Dockerfile b/containers/openstack-client/Dockerfile index bd8e9dc12..eb80195af 100644 --- a/containers/openstack-client/Dockerfile +++ b/containers/openstack-client/Dockerfile @@ -1,4 +1,19 @@ # syntax=docker/dockerfile:1 ARG OPENSTACK_VERSION="required_argument" -FROM quay.io/airshipit/openstack-client:${OPENSTACK_VERSION}-ubuntu_noble +FROM quay.io/airshipit/openstack-client:${OPENSTACK_VERSION}-ubuntu_noble AS build + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken + +ARG OPENSTACK_VERSION="required_argument" +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install \ + --upgrade \ + --constraint https://releases.openstack.org/constraints/upper/${OPENSTACK_VERSION} \ + /src/keystoneauth-kubeservicetoken + +ARG OPENSTACK_VERSION="required_argument" +FROM quay.io/airshipit/openstack-client:${OPENSTACK_VERSION}-ubuntu_noble AS final +COPY --from=build --link /var/lib/openstack /var/lib/openstack From 880a9620169a5aa3268249645b1588ad24577435 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 8 Jun 2026 17:06:16 +0100 Subject: [PATCH 21/33] keystone: use stringData in generated secrets to fix base64 errors K8s Secret data fields require base64-encoded values; stringData accepts plain text and lets the API server handle encoding. --- components/openstack/templates/keystone-service-user.yaml.tpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/openstack/templates/keystone-service-user.yaml.tpl b/components/openstack/templates/keystone-service-user.yaml.tpl index 6dac274ef..861e145e3 100644 --- a/components/openstack/templates/keystone-service-user.yaml.tpl +++ b/components/openstack/templates/keystone-service-user.yaml.tpl @@ -6,7 +6,7 @@ apiVersion: v1 kind: Secret metadata: name: {{ (printf "%s-keystone-%s" $serviceName $user.usage) | quote }} -data: +stringData: OS_AUTH_TYPE: v3oidcaccesstokenfile OS_AUTH_URL: {{ $.Values.keystoneUrl | quote }} OS_DEFAULT_DOMAIN: "default" @@ -27,7 +27,7 @@ apiVersion: v1 kind: Secret metadata: name: {{ (printf "%s-ks-etc" $serviceName) | quote }} -data: +stringData: {{ (printf "%s_auth.conf" $serviceName) | quote }}: | {{- range $_, $user := $users }} {{- $section := $user.section | default $user.usage }} From e6f5e3aec9d93718e8b9280c684ef008d7410c53 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 8 Jun 2026 17:59:25 +0100 Subject: [PATCH 22/33] keystone: add admin@Default access for k8s --- ansible/roles/keystone_bootstrap/defaults/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ansible/roles/keystone_bootstrap/defaults/main.yml b/ansible/roles/keystone_bootstrap/defaults/main.yml index b05784e31..46791bb75 100644 --- a/ansible/roles/keystone_bootstrap/defaults/main.yml +++ b/ansible/roles/keystone_bootstrap/defaults/main.yml @@ -94,3 +94,6 @@ keystone_bootstrap_k8s_groups: - project: baremetal domain: infra role: service + - project: admin + domain: Default + role: admin From c616b7354ceae483d31e6a608fbd5c443d347151 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 15 Jun 2026 11:53:46 +0100 Subject: [PATCH 23/33] fix: keystone-service-user reference to ID provider --- components/openstack/templates/keystone-service-user.yaml.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/openstack/templates/keystone-service-user.yaml.tpl b/components/openstack/templates/keystone-service-user.yaml.tpl index 861e145e3..20ceb556b 100644 --- a/components/openstack/templates/keystone-service-user.yaml.tpl +++ b/components/openstack/templates/keystone-service-user.yaml.tpl @@ -37,7 +37,7 @@ stringData: [{{ $section }}] auth_type=v3oidcaccesstokenfile auth_url={{ $.Values.keystoneUrl }} - identity_provider={{ $.Values.identityProvider }} + identity_provider={{ $.Values.keystoneServiceUsers.keystoneIdentityProvider }} protocol=openid access_token_file=/var/run/secrets/kubernetes.io/serviceaccount/token region_name={{ $.Values.regionName }} From c5b0f1e3456948a5d6d8c486400feec342d34d73 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 15 Jun 2026 13:34:31 +0100 Subject: [PATCH 24/33] fix: include keystoneauth-kubeservicetoken in ironic --- containers/ironic/Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/containers/ironic/Dockerfile b/containers/ironic/Dockerfile index e669eb74c..929758463 100644 --- a/containers/ironic/Dockerfile +++ b/containers/ironic/Dockerfile @@ -23,6 +23,7 @@ RUN git -C /src/sushy fetch --unshallow --tags && \ sed -i '/sushy==.*/d' /upper-constraints.txt COPY python/ironic-understack /src/understack/ironic-understack +COPY python/keystoneauth-kubeservicetoken /src/keystoneauth-kubeservicetoken ARG OPENSTACK_VERSION="required_argument" RUN --mount=type=cache,target=/root/.cache/uv \ @@ -32,7 +33,8 @@ RUN --mount=type=cache,target=/root/.cache/uv \ /src/ironic \ /src/sushy \ /src/understack/ironic-understack \ - proliantutils==2.16.3 + /src/keystoneauth-kubeservicetoken \ + proliantutils==2.16.3 COPY containers/ironic/patches /tmp/patches/ RUN cd /var/lib/openstack/lib/python3.12/site-packages && \ From c6610b3b90aa68e654633af0e32872942b04ff66 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 15 Jun 2026 14:58:52 +0100 Subject: [PATCH 25/33] fix: restore project_name and project_domain_name Without this, the auth plugin switches back to requesting unscoped tokens which results in the empty service catalog and services not working. --- components/openstack/templates/keystone-service-user.yaml.tpl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/openstack/templates/keystone-service-user.yaml.tpl b/components/openstack/templates/keystone-service-user.yaml.tpl index 20ceb556b..28d878d77 100644 --- a/components/openstack/templates/keystone-service-user.yaml.tpl +++ b/components/openstack/templates/keystone-service-user.yaml.tpl @@ -41,6 +41,8 @@ stringData: protocol=openid access_token_file=/var/run/secrets/kubernetes.io/serviceaccount/token region_name={{ $.Values.regionName }} + project_domain_name={{ include "openstack.serviceuser.project_domain_name" $user }} + project_name={{ include "openstack.serviceuser.project_name" $user }} {{- end }} {{- end }} {{- end }} From 95419ea32512315640b357aba3fc12c4ec535d59 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 17 Jun 2026 12:08:38 +0100 Subject: [PATCH 26/33] drop! fix: set image for neutron_ironic_agent --- components/images-openstack.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/components/images-openstack.yaml b/components/images-openstack.yaml index 7a8322250..c53905c90 100644 --- a/components/images-openstack.yaml +++ b/components/images-openstack.yaml @@ -39,6 +39,7 @@ images: neutron_l3: "ghcr.io/rackerlabs/understack/neutron:pr-1876" neutron_l2gw: "ghcr.io/rackerlabs/understack/neutron:pr-1876" neutron_linuxbridge_agent: "ghcr.io/rackerlabs/understack/neutron:pr-1876" + neutron_ironic_agent: "ghcr.io/rackerlabs/understack/neutron:pr-1876" neutron_metadata: "ghcr.io/rackerlabs/understack/neutron:pr-1876" neutron_ovn_metadata: "ghcr.io/rackerlabs/understack/neutron:pr-1876" neutron_openvswitch_agent: "ghcr.io/rackerlabs/understack/neutron:pr-1876" From dec6e5dd7d543f3c904ce88b1a9cb3c994dfa84e Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 6 Jul 2026 10:03:35 +0100 Subject: [PATCH 27/33] chore: upgrade OSH charts again This is following merge of https://review.opendev.org/c/openstack/openstack-helm/+/993881 --- charts/argocd-understack/values.yaml | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/charts/argocd-understack/values.yaml b/charts/argocd-understack/values.yaml index 53a9c84d9..9163ea5f0 100644 --- a/charts/argocd-understack/values.yaml +++ b/charts/argocd-understack/values.yaml @@ -146,7 +146,7 @@ global: enabled: false # -- config file to use for Nautobot scoped to either $understack or $deploy repo # @default -- $understack/components/nautobot/nautobot_config.py - nautobot_config: '$understack/components/nautobot/nautobot_config.py' + nautobot_config: "$understack/components/nautobot/nautobot_config.py" # -- Nautobot API token generation jobs nautobot_api_tokens: @@ -235,7 +235,6 @@ global: # @default -- false enabled: false - # -- This block is for setting up the UnderStack site specific ArgoCD Applications site: # -- Enable/disable deploying the site specific applications @@ -347,9 +346,8 @@ site: wave: 1 # -- Chart version for Keystone # renovate: datasource=helm depName=keystone registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.5+a2a343968 + chartVersion: 2026.1.5+a659ab8a2f # -- Glance (Image Service) - # -- Glance (Image Service) glance: # -- Enable/disable deploying Glance # @default -- false @@ -359,7 +357,7 @@ site: wave: 2 # -- Chart version for Glance # renovate: datasource=helm depName=glance registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.5+bad57716b + chartVersion: 2026.1.5+a659ab8a2f # -- Cinder (Block Storage Service) cinder: @@ -371,8 +369,7 @@ site: wave: 2 # -- Chart version for Cinder # renovate: datasource=helm depName=cinder registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.5+872fd69e7 - + chartVersion: 2026.1.5+a659ab8a2f # -- Ironic (Bare Metal Service) ironic: # -- Enable/disable deploying Ironic @@ -383,8 +380,7 @@ site: wave: 2 # -- Chart version for Ironic # renovate: datasource=helm depName=ironic registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.7+872fd69e7 - + chartVersion: 2026.1.7+a659ab8a2f # -- Neutron (Networking Service) neutron: # -- Enable/disable deploying Neutron @@ -395,7 +391,7 @@ site: wave: 2 # -- Chart version for Neutron # renovate: datasource=helm depName=neutron registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.8+872fd69e7 + chartVersion: 2026.1.8+a659ab8a2f # -- Placement (Placement Service) placement: @@ -431,7 +427,7 @@ site: wave: 3 # -- Chart version for Octavia # renovate: datasource=helm depName=octavia registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.4+872fd69e7 + chartVersion: 2026.1.4+a659ab8a2f # -- Horizon (Dashboard) horizon: @@ -443,7 +439,7 @@ site: wave: 4 # -- Chart version for Horizon # renovate: datasource=helm depName=horizon registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.2+872fd69e7 + chartVersion: 2026.1.2+a659ab8a2f # -- Skyline (Dashboard) skyline: @@ -455,7 +451,7 @@ site: wave: 4 # -- Chart version for Skyline # renovate: datasource=helm depName=skyline registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.2+872fd69e7 + chartVersion: 2026.1.2+a659ab8a2f # -- Open vSwitch (Networking) openvswitch: @@ -587,7 +583,7 @@ site: enabled: false # -- config file to use for Nautobot scoped to either $understack or $deploy repo # @default -- $understack/components/nautobot/nautobot_config.py - nautobot_config: '$understack/components/nautobot/nautobot_config.py' + nautobot_config: "$understack/components/nautobot/nautobot_config.py" # -- Ironic hardware exporter for bare-metal hardware metrics via RabbitMQ ironic_hardware_exporter: From e5b035ae65b34550d7dccd6b46c42585f90e6aa5 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 6 Jul 2026 10:20:14 +0100 Subject: [PATCH 28/33] fix: missed the image upgrade for keystone --- components/images-openstack.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/components/images-openstack.yaml b/components/images-openstack.yaml index c53905c90..7fae0af94 100644 --- a/components/images-openstack.yaml +++ b/components/images-openstack.yaml @@ -13,13 +13,13 @@ images: ks_endpoints: "ghcr.io/rackerlabs/understack/openstack-client:pr-1876" # keystone - keystone_api: "ghcr.io/rackerlabs/understack/keystone:2025.2" - keystone_credential_rotate: "ghcr.io/rackerlabs/understack/keystone:2025.2" - keystone_credential_setup: "ghcr.io/rackerlabs/understack/keystone:2025.2" - keystone_db_sync: "ghcr.io/rackerlabs/understack/keystone:2025.2" - keystone_domain_manage: "ghcr.io/rackerlabs/understack/keystone:2025.2" - keystone_fernet_rotate: "ghcr.io/rackerlabs/understack/keystone:2025.2" - keystone_fernet_setup: "ghcr.io/rackerlabs/understack/keystone:2025.2" + keystone_api: "ghcr.io/rackerlabs/understack/keystone:pr-1876" + keystone_credential_rotate: "ghcr.io/rackerlabs/understack/keystone:pr-1876" + keystone_credential_setup: "ghcr.io/rackerlabs/understack/keystone:pr-1876" + keystone_db_sync: "ghcr.io/rackerlabs/understack/keystone:pr-1876" + keystone_domain_manage: "ghcr.io/rackerlabs/understack/keystone:pr-1876" + keystone_fernet_rotate: "ghcr.io/rackerlabs/understack/keystone:pr-1876" + keystone_fernet_setup: "ghcr.io/rackerlabs/understack/keystone:pr-1876" # ironic ironic_api: "ghcr.io/rackerlabs/understack/ironic:pr-1876" From b722b2cc8eb34927b9aea1cabcd73f611d15de14 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 6 Jul 2026 11:13:17 +0100 Subject: [PATCH 29/33] fix: update placement chart too --- charts/argocd-understack/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/argocd-understack/values.yaml b/charts/argocd-understack/values.yaml index 9163ea5f0..8f152b0a9 100644 --- a/charts/argocd-understack/values.yaml +++ b/charts/argocd-understack/values.yaml @@ -403,7 +403,7 @@ site: wave: 2 # -- Chart version for Placement # renovate: datasource=helm depName=placement registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.4+a2a343968 + chartVersion: 2026.1.4+a659ab8a2f # -- Nova (Compute Service) nova: From 9f326dd88b622bff28c16fef13c8c8c443d66278 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 6 Jul 2026 11:45:42 +0100 Subject: [PATCH 30/33] rax-dev-iad3-dev: upgrade nova chart --- charts/argocd-understack/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/argocd-understack/values.yaml b/charts/argocd-understack/values.yaml index 8f152b0a9..7b4da2e74 100644 --- a/charts/argocd-understack/values.yaml +++ b/charts/argocd-understack/values.yaml @@ -415,7 +415,7 @@ site: wave: 3 # -- Chart version for Nova # renovate: datasource=helm depName=nova registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.8+a2a343968 + chartVersion: 2026.1.8+a659ab8a2f # -- Octavia (Load Balancer Service) octavia: From 09aa5fda188754a6b5f00cc488940e32a9ecf177 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 8 Jul 2026 10:18:20 +0100 Subject: [PATCH 31/33] fix the chart commit ID again --- charts/argocd-understack/values.yaml | 20 +++--- .../design.md | 69 +++++++++++++++++++ .../proposal.md | 26 +++++++ .../file-based-oidc-access-token-auth/spec.md | 41 +++++++++++ .../tasks.md | 29 ++++++++ .../openspec/config.yaml | 22 ++++++ 6 files changed, 197 insertions(+), 10 deletions(-) create mode 100644 python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/design.md create mode 100644 python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/proposal.md create mode 100644 python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/specs/file-based-oidc-access-token-auth/spec.md create mode 100644 python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/tasks.md create mode 100644 python/keystoneauth-kubeservicetoken/openspec/config.yaml diff --git a/charts/argocd-understack/values.yaml b/charts/argocd-understack/values.yaml index 7b4da2e74..a536e01ea 100644 --- a/charts/argocd-understack/values.yaml +++ b/charts/argocd-understack/values.yaml @@ -346,7 +346,7 @@ site: wave: 1 # -- Chart version for Keystone # renovate: datasource=helm depName=keystone registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.5+a659ab8a2f # -- Glance (Image Service) + chartVersion: 2026.1.5+a659ab8a2 # -- Glance (Image Service) glance: # -- Enable/disable deploying Glance @@ -357,7 +357,7 @@ site: wave: 2 # -- Chart version for Glance # renovate: datasource=helm depName=glance registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.5+a659ab8a2f + chartVersion: 2026.1.5+a659ab8a2 # -- Cinder (Block Storage Service) cinder: @@ -369,7 +369,7 @@ site: wave: 2 # -- Chart version for Cinder # renovate: datasource=helm depName=cinder registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.5+a659ab8a2f + chartVersion: 2026.1.5+a659ab8a2 # -- Ironic (Bare Metal Service) ironic: # -- Enable/disable deploying Ironic @@ -380,7 +380,7 @@ site: wave: 2 # -- Chart version for Ironic # renovate: datasource=helm depName=ironic registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.7+a659ab8a2f + chartVersion: 2026.1.7+a659ab8a2 # -- Neutron (Networking Service) neutron: # -- Enable/disable deploying Neutron @@ -391,7 +391,7 @@ site: wave: 2 # -- Chart version for Neutron # renovate: datasource=helm depName=neutron registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.8+a659ab8a2f + chartVersion: 2026.1.8+a659ab8a2 # -- Placement (Placement Service) placement: @@ -403,7 +403,7 @@ site: wave: 2 # -- Chart version for Placement # renovate: datasource=helm depName=placement registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.4+a659ab8a2f + chartVersion: 2026.1.4+a659ab8a2 # -- Nova (Compute Service) nova: @@ -415,7 +415,7 @@ site: wave: 3 # -- Chart version for Nova # renovate: datasource=helm depName=nova registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.8+a659ab8a2f + chartVersion: 2026.1.13+a659ab8a2 # -- Octavia (Load Balancer Service) octavia: @@ -427,7 +427,7 @@ site: wave: 3 # -- Chart version for Octavia # renovate: datasource=helm depName=octavia registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.4+a659ab8a2f + chartVersion: 2026.1.4+a659ab8a2 # -- Horizon (Dashboard) horizon: @@ -439,7 +439,7 @@ site: wave: 4 # -- Chart version for Horizon # renovate: datasource=helm depName=horizon registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.2+a659ab8a2f + chartVersion: 2026.1.2+a659ab8a2 # -- Skyline (Dashboard) skyline: @@ -451,7 +451,7 @@ site: wave: 4 # -- Chart version for Skyline # renovate: datasource=helm depName=skyline registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.2+a659ab8a2f + chartVersion: 2026.1.2+a659ab8a2 # -- Open vSwitch (Networking) openvswitch: diff --git a/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/design.md b/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/design.md new file mode 100644 index 000000000..42a8cb30b --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/design.md @@ -0,0 +1,69 @@ +## Context + +This repository will provide a custom keystoneauth plugin for workloads that already rotate OIDC access tokens into a local file (for example, Kubernetes projected service account tokens or an external refresh process). The upstream `keystoneauth1.identity.v3.OpenIDConnectAccessToken` plugin expects an inline `access_token`, which does not work well when tokens are short-lived and updated frequently. The plugin must remain compatible with OpenStack service integration patterns used by nova, ironic, and neutron. + +## Goals / Non-Goals + +**Goals:** +- Provide a plugin that extends `keystoneauth1.identity.v3.OpenIDConnectAccessToken` and accepts a token file path instead of a static `access_token` value. +- Read the current token from disk when authenticating with Keystone. +- Align token file re-read behavior with Keystone token renewal timing so rotated OIDC tokens are picked up without reauthenticating on every request. +- Keep configuration and usage simple for service operators. + +**Non-Goals:** +- Implement a token refresh service or write new OIDC tokens to disk. +- Replace or modify upstream keystoneauth caching internals beyond what is needed for this plugin. +- Change Keystone server-side behavior, token formats, or federation configuration. + +## Decisions + +### 1) Inherit from `OpenIDConnectAccessToken` +The custom class will inherit from `keystoneauth1.identity.v3.OpenIDConnectAccessToken` to reuse existing auth payload construction and Keystone interaction behavior. + +Alternatives considered: +- Build a plugin from scratch on `v3.Auth`: rejected because it duplicates existing OIDC access-token auth behavior and increases maintenance. + +### 2) Introduce `access_token_file` as the primary input +The plugin will accept a required file path option (for example, `access_token_file`) and will not require callers to pass inline `access_token`. + +Alternatives considered: +- Support both `access_token` and `access_token_file` with precedence rules: rejected for initial scope to avoid ambiguous configuration and operator mistakes. + +### 3) Read token lazily at authentication time +The plugin will load the token from disk only when it needs to authenticate/re-authenticate with Keystone. This ensures the token is as fresh as possible at the point where it matters. + +Alternatives considered: +- Poll or watch the file continuously: rejected due to unnecessary complexity and overhead. + +### 4) Preserve keystoneauth session/token caching behavior +The plugin will rely on keystoneauth's normal Keystone token caching. When the Keystone token is still valid, no reauthentication occurs and file changes are ignored. When the Keystone token is near expiry and reauthentication is triggered, the plugin re-reads the file and uses the latest OIDC token. + +Alternatives considered: +- Force reauthentication whenever the file changes: rejected because the requirement is to reauthenticate near Keystone token expiry, not on every file update. + +### 5) Fail fast on invalid file state +If the configured file is missing, unreadable, or empty during authentication, the plugin should raise a clear authentication-related error so operators can diagnose configuration/runtime issues quickly. + +Alternatives considered: +- Silent fallback to previous token value: rejected because it can mask rotation and operational issues. + +## Risks / Trade-offs + +- [Token file rotates between read and auth request] -> Mitigation: read token immediately before building auth payload; rely on subsequent retries/reauth if needed. +- [Unreadable or malformed token file causes auth failures] -> Mitigation: clear errors, input trimming/validation, and test coverage for failure modes. +- [Different service process privileges affect file readability] -> Mitigation: document required file permissions and deployment expectations. +- [Assuming upstream reauth timing behavior] -> Mitigation: add tests/assertions around reauthentication flow to validate token file is consulted when Keystone token renewal occurs. + +## Migration Plan + +1. Implement and package the plugin in this repository. +2. Add operator-facing usage documentation and config examples for nova/ironic/neutron style keystoneauth options. +3. Update service configuration to reference the new plugin and set `access_token_file`. +4. Roll out in non-production first; verify successful authentication and reauthentication across at least one token rotation cycle. +5. Rollback strategy: switch service config back to the previous auth plugin/value and restart/reload services. + +## Open Questions + +- What final config option name should be exposed (`access_token_file` vs `token_file`) to best match keystoneauth conventions? +- Should optional behavior allow reading a custom file encoding, or is UTF-8-only sufficient? +- Do target services require any explicit entry-point naming conventions for discovery beyond standard keystoneauth plugin registration? diff --git a/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/proposal.md b/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/proposal.md new file mode 100644 index 000000000..b498f3185 --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/proposal.md @@ -0,0 +1,26 @@ +## Why + +OpenStack services that use `keystoneauth1.identity.v3.OpenIDConnectAccessToken` require a static `access_token` value, which is a poor fit for short-lived OIDC tokens refreshed outside the process. We need a plugin that reads the current token from a file and integrates cleanly with Keystone token lifecycle behavior. + +## What Changes + +- Add a custom auth plugin in this repository that extends `keystoneauth1.identity.v3.OpenIDConnectAccessToken`. +- Replace direct `access_token` configuration with a required token file path parameter. +- Read the OIDC access token from disk at authentication time. +- Ensure token-file updates are honored when Keystone token renewal is triggered (near Keystone token expiry), without forcing reauthentication on every request. +- Keep compatibility with OpenStack service consumers such as nova, ironic, and neutron that use keystoneauth plugin loading patterns. + +## Capabilities + +### New Capabilities +- `file-based-oidc-access-token-auth`: Authenticate to Keystone using an OIDC access token read from a configured file path, with refresh behavior aligned to Keystone token reauthentication timing. + +### Modified Capabilities +- None. + +## Impact + +- Affected code: new plugin module(s), plugin option handling, and tests in this repository. +- API/config impact: introduces a file-path based token input (instead of requiring inline `access_token`) for this custom plugin. +- Runtime behavior: token file is re-read when Keystone token reauthentication occurs near expiry, so externally rotated OIDC tokens are picked up. +- Dependencies: continues to rely on `keystoneauth1`; no new external service dependency is expected. diff --git a/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/specs/file-based-oidc-access-token-auth/spec.md b/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/specs/file-based-oidc-access-token-auth/spec.md new file mode 100644 index 000000000..6dbc91527 --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/specs/file-based-oidc-access-token-auth/spec.md @@ -0,0 +1,41 @@ +## ADDED Requirements + +### Requirement: Plugin accepts token file configuration +The authentication plugin SHALL require a configuration option that identifies the filesystem path of the OIDC access token file and SHALL use this option as the token source for authentication payload generation. + +#### Scenario: Valid token file path is configured +- **WHEN** the plugin is initialized with a readable token file path +- **THEN** the plugin accepts the configuration and is ready to authenticate + +#### Scenario: Token file path is missing +- **WHEN** the plugin is initialized without the required token file path option +- **THEN** initialization or first authentication attempt MUST fail with a clear configuration error + +### Requirement: Plugin reads token from file at authentication time +The plugin SHALL read the access token value from the configured file each time it performs Keystone authentication or reauthentication, and SHALL use the read value as the OIDC access token in the auth request. + +#### Scenario: File contains a valid token +- **WHEN** the plugin performs authentication and the configured file contains a non-empty token value +- **THEN** the plugin sends that token value in the Keystone authentication request + +#### Scenario: File is unreadable or empty +- **WHEN** the plugin performs authentication and the token file is unreadable, missing, or empty +- **THEN** the plugin MUST fail authentication with an explicit file/token read error + +### Requirement: Reauthentication behavior follows Keystone token lifecycle +The plugin MUST preserve normal keystoneauth Keystone token caching behavior and MUST NOT force reauthentication solely because the token file content changed while the Keystone token remains valid. + +#### Scenario: Token file changes while Keystone token is still valid +- **WHEN** the configured token file is updated after a successful Keystone authentication +- **THEN** the existing Keystone token continues to be used until reauthentication is triggered by keystoneauth lifecycle rules + +#### Scenario: Keystone token nears expiry +- **WHEN** keystoneauth triggers reauthentication because the Keystone token is near expiry +- **THEN** the plugin re-reads the configured token file and uses the latest token value for the new authentication request + +### Requirement: Plugin is usable by OpenStack service consumers +The plugin SHALL be discoverable and configurable through standard keystoneauth plugin loading conventions so OpenStack services such as nova, ironic, and neutron can use it without custom integration code. + +#### Scenario: Service loads plugin by configured auth type +- **WHEN** a service references the plugin's auth type and required options in its Keystone auth configuration +- **THEN** keystoneauth loads the plugin and performs authentication successfully using the file-sourced OIDC token diff --git a/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/tasks.md b/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/tasks.md new file mode 100644 index 000000000..06683d00d --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/openspec/changes/add-file-based-oidc-access-token-plugin/tasks.md @@ -0,0 +1,29 @@ +## 1. Plugin Interface and Configuration + +- [x] 1.1 Create a custom plugin class that inherits from `keystoneauth1.identity.v3.oidc.OidcAccessToken` and introduces `access_token_file` configuration. +- [x] 1.2 Implement a custom loader/options definition that requires `access_token_file` and does not require inline `access_token`/`auth_token` configuration. +- [x] 1.3 Ensure inherited plugin behavior still works when `access_token`/`auth_token` is not provided by reading and supplying token value from file internally. +- [x] 1.4 Add loader and initialization tests proving plugin creation succeeds with only `access_token_file` (plus required federation options). + +## 2. Token File Read and Auth Flow + +- [x] 2.1 Implement token-file read logic used at authentication/reauthentication time, including whitespace trimming. +- [x] 2.2 Integrate file-sourced token into the inherited OIDC access-token auth request flow. +- [x] 2.3 Implement explicit failure handling for missing, unreadable, or empty token files. + +## 3. Reauthentication and Caching Semantics + +- [x] 3.1 Ensure implementation preserves normal Keystone token caching and does not force reauthentication while current Keystone token remains valid. +- [x] 3.2 Verify token file is re-read when keystoneauth triggers reauthentication near Keystone token expiry. +- [x] 3.3 Add tests that token-file updates are consumed on reauthentication timing rather than on every request. + +## 4. Plugin Registration and Service Compatibility + +- [x] 4.1 Register plugin entry point/auth type so it is discoverable through standard keystoneauth plugin loading. +- [x] 4.2 Add service-facing configuration examples for nova, ironic, and neutron style auth settings. + +## 5. Validation and Documentation + +- [x] 5.1 Add unit tests for successful auth, missing file-path option, and token file failure modes. +- [x] 5.2 Run targeted and/or full test suite and resolve regressions. +- [x] 5.3 Document rollout and rollback notes for operators migrating from inline access-token config. diff --git a/python/keystoneauth-kubeservicetoken/openspec/config.yaml b/python/keystoneauth-kubeservicetoken/openspec/config.yaml new file mode 100644 index 000000000..ae7fa21f3 --- /dev/null +++ b/python/keystoneauth-kubeservicetoken/openspec/config.yaml @@ -0,0 +1,22 @@ +schema: spec-driven +context: | + ## Documentation + + The source code of upstream keystoneauth is available in /home/skrobul/devel/keystoneauth/ which can be used to reference upstream code without need of consulting context7 MCP for documentation. + + + ## Python environment + If any python code needs to be ran, make sure to: + - use `uv` for module and requirement management of pyproject.toml + - run any code through venv generated/managed by uv + + +# Per-artifact rules (optional) +# Add custom rules for specific artifacts. +# Example: +# rules: +# proposal: +# - Keep proposals under 500 words +# - Always include a "Non-goals" section +# tasks: +# - Break tasks into chunks of max 2 hours From 337cfcc7c20963c7e2e2e55aaae29a2708d24b61 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 8 Jul 2026 10:31:35 +0100 Subject: [PATCH 32/33] rax-dev-iad3-dev: fix stale chart version pins for a659ab8a2 09aa5fda removed the stray trailing "f" from the a659ab8a2 hash but left the version numbers unchanged. Helm's chart-repo resolution ignores semver build metadata (the "+hash" suffix) when matching, so each pin was silently resolving to whatever pre-fix build already existed under that version number instead of erroring. Bump every chart's version number to the one actually published alongside a659ab8a2 (verified against the tarballs.opendev.org index) so the identity.openrc wiring fix actually gets deployed. --- charts/argocd-understack/values.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/charts/argocd-understack/values.yaml b/charts/argocd-understack/values.yaml index a536e01ea..50bfc7ced 100644 --- a/charts/argocd-understack/values.yaml +++ b/charts/argocd-understack/values.yaml @@ -346,7 +346,7 @@ site: wave: 1 # -- Chart version for Keystone # renovate: datasource=helm depName=keystone registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.5+a659ab8a2 # -- Glance (Image Service) + chartVersion: 2026.1.10+a659ab8a2 # -- Glance (Image Service) glance: # -- Enable/disable deploying Glance @@ -357,7 +357,7 @@ site: wave: 2 # -- Chart version for Glance # renovate: datasource=helm depName=glance registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.5+a659ab8a2 + chartVersion: 2026.1.11+a659ab8a2 # -- Cinder (Block Storage Service) cinder: @@ -369,7 +369,7 @@ site: wave: 2 # -- Chart version for Cinder # renovate: datasource=helm depName=cinder registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.5+a659ab8a2 + chartVersion: 2026.1.14+a659ab8a2 # -- Ironic (Bare Metal Service) ironic: # -- Enable/disable deploying Ironic @@ -380,7 +380,7 @@ site: wave: 2 # -- Chart version for Ironic # renovate: datasource=helm depName=ironic registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.7+a659ab8a2 + chartVersion: 2026.1.10+a659ab8a2 # -- Neutron (Networking Service) neutron: # -- Enable/disable deploying Neutron @@ -391,7 +391,7 @@ site: wave: 2 # -- Chart version for Neutron # renovate: datasource=helm depName=neutron registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.8+a659ab8a2 + chartVersion: 2026.1.15+a659ab8a2 # -- Placement (Placement Service) placement: @@ -403,7 +403,7 @@ site: wave: 2 # -- Chart version for Placement # renovate: datasource=helm depName=placement registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.4+a659ab8a2 + chartVersion: 2026.1.7+a659ab8a2 # -- Nova (Compute Service) nova: @@ -427,7 +427,7 @@ site: wave: 3 # -- Chart version for Octavia # renovate: datasource=helm depName=octavia registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.4+a659ab8a2 + chartVersion: 2026.1.7+a659ab8a2 # -- Horizon (Dashboard) horizon: @@ -439,7 +439,7 @@ site: wave: 4 # -- Chart version for Horizon # renovate: datasource=helm depName=horizon registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.2+a659ab8a2 + chartVersion: 2026.1.8+a659ab8a2 # -- Skyline (Dashboard) skyline: @@ -451,7 +451,7 @@ site: wave: 4 # -- Chart version for Skyline # renovate: datasource=helm depName=skyline registryUrl=https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2026.1.2+a659ab8a2 + chartVersion: 2026.1.3+a659ab8a2 # -- Open vSwitch (Networking) openvswitch: From e2346603730b2f99c9488f12f3faf3c124c25913 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Wed, 8 Jul 2026 11:09:57 +0100 Subject: [PATCH 33/33] cinder: stop double-declaring the cinder-etc-snippets volume The chart gained native pod.etcSources support for cinder-ks-etc back in Feb 2026 (upstream commit 06a5261a0), which unconditionally declares a cinder-etc-snippets volume (projected, or an emptyDir fallback when etcSources is empty). Our own pod.mounts override predates that (Sep 2025) and injects a volume of the same name, so every cinder Deployment/CronJob ended up with two "cinder-etc-snippets" volumes. Switch to pod.etcSources for the components that support it and drop the now-redundant custom mounts. cinder_db_sync keeps its manual mount since that job template has no etcSources support. --- components/cinder/values.yaml | 93 +++++++++-------------------------- 1 file changed, 24 insertions(+), 69 deletions(-) diff --git a/components/cinder/values.yaml b/components/cinder/values.yaml index 3a5099a75..43f420a0f 100644 --- a/components/cinder/values.yaml +++ b/components/cinder/values.yaml @@ -73,32 +73,11 @@ pod: volumeMounts: - mountPath: /var/lib/cinder name: var-lib-cinder - - name: cinder-etc-snippets - mountPath: /etc/cinder/cinder.conf.d/ - readOnly: true volumes: - name: var-lib-cinder emptyDir: {} - - name: cinder-etc-snippets - projected: - sources: - - secret: - name: cinder-ks-etc - - secret: - name: cinder-netapp-config - optional: true - cinder_volume_usage_audit: - cinder_volume_usage_audit: - volumeMounts: - - name: cinder-etc-snippets - mountPath: /etc/cinder/cinder.conf.d/ - readOnly: true - volumes: - - name: cinder-etc-snippets - projected: - sources: - - secret: - name: cinder-ks-etc + # cinder_db_sync has no native etcSources support in the chart, so the + # cinder-etc-snippets mount still has to be injected manually here. cinder_db_sync: cinder_db_sync: volumeMounts: @@ -111,54 +90,30 @@ pod: sources: - secret: name: cinder-ks-etc - cinder_backup: - cinder_backup: - volumeMounts: - - name: cinder-etc-snippets - mountPath: /etc/cinder/cinder.conf.d/ - readOnly: true - volumes: - - name: cinder-etc-snippets - projected: - sources: - - secret: - name: cinder-ks-etc + # cinder-ks-etc mounting for the other components is handled natively via + # pod.etcSources below, rather than duplicating it through pod.mounts. + etcSources: + cinder_api: + - secret: + name: cinder-ks-etc cinder_scheduler: - cinder_scheduler: - volumeMounts: - - name: cinder-etc-snippets - mountPath: /etc/cinder/cinder.conf.d/ - readOnly: true - volumes: - - name: cinder-etc-snippets - projected: - sources: - - secret: - name: cinder-ks-etc + - secret: + name: cinder-ks-etc + cinder_volume: + - secret: + name: cinder-ks-etc + - secret: + name: cinder-netapp-config + optional: true + cinder_volume_usage_audit: + - secret: + name: cinder-ks-etc + cinder_backup: + - secret: + name: cinder-ks-etc cinder_db_purge: - cinder_db_purge: - volumeMounts: - - name: cinder-etc-snippets - mountPath: /etc/cinder/cinder.conf.d/ - readOnly: true - volumes: - - name: cinder-etc-snippets - projected: - sources: - - secret: - name: cinder-ks-etc - cinder_api: - cinder_api: - volumeMounts: - - name: cinder-etc-snippets - mountPath: /etc/cinder/cinder.conf.d/ - readOnly: true - volumes: - - name: cinder-etc-snippets - projected: - sources: - - secret: - name: cinder-ks-etc + - secret: + name: cinder-ks-etc lifecycle: disruption_budget: deployments: