Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-castellan for 
openSUSE:Factory checked in at 2026-08-19 18:00:26
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-castellan (Old)
 and      /work/SRC/openSUSE:Factory/.python-castellan.new.1258 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-castellan"

Wed Aug 19 18:00:26 2026 rev:24 rq:1371531 version:5.8.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-castellan/python-castellan.changes        
2026-06-03 20:25:49.925697427 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-castellan.new.1258/python-castellan.changes  
    2026-08-19 18:02:52.085788875 +0200
@@ -1,0 +2,12 @@
+Wed Aug  5 15:12:03 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 5.8.0:
+  * Drop Python 3.10 support
+  * zuul: Use openstack-python3-next-jobs template
+  * pre-commit: Bump versions
+  * Do not install code to build release notes
+  * vault: Apply auth_path for approle auth method
+  * Add token-based auth method to VaultKeyManager
+  * Do not expose internal client exceptions
+
+-------------------------------------------------------------------

Old:
----
  castellan-5.7.0.tar.gz

New:
----
  castellan-5.8.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-castellan.spec ++++++
--- /var/tmp/diff_new_pack.IbpCOR/_old  2026-08-19 18:02:52.764813229 +0200
+++ /var/tmp/diff_new_pack.IbpCOR/_new  2026-08-19 18:02:52.766813301 +0200
@@ -18,7 +18,7 @@
 
 %global pythons %{primary_python}
 Name:           python-castellan
-Version:        5.7.0
+Version:        5.8.0
 Release:        0
 Summary:        Generic Key Manager interface for OpenStack
 License:        Apache-2.0

++++++ castellan-5.7.0.tar.gz -> castellan-5.8.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/castellan-5.7.0/.pre-commit-config.yaml 
new/castellan-5.8.0/.pre-commit-config.yaml
--- old/castellan-5.7.0/.pre-commit-config.yaml 2026-05-18 11:02:59.000000000 
+0200
+++ new/castellan-5.8.0/.pre-commit-config.yaml 2026-07-10 15:40:18.000000000 
+0200
@@ -13,13 +13,13 @@
       - id: check-yaml
         files: .*\.(yaml|yml)$
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.15.10
+    rev: v0.15.18
     hooks:
       - id: ruff-check
         args: ['--fix', '--unsafe-fixes']
       - id: ruff-format
   - repo: https://opendev.org/openstack/hacking
-    rev: 8.0.0
+    rev: 8.1.0
     hooks:
       - id: hacking
         additional_dependencies: []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/castellan-5.7.0/.zuul.yaml 
new/castellan-5.8.0/.zuul.yaml
--- old/castellan-5.7.0/.zuul.yaml      2026-05-18 11:02:59.000000000 +0200
+++ new/castellan-5.8.0/.zuul.yaml      2026-07-10 15:40:18.000000000 +0200
@@ -1,3 +1,5 @@
+---
+
 - job:
     name: castellan-functional-vault
     parent: openstack-tox
@@ -52,6 +54,7 @@
     templates:
       - check-requirements
       - openstack-python3-jobs
+      - openstack-python3-next-jobs
       - periodic-stable-jobs
       - publish-openstack-docs-pti
       - release-notes-jobs-python3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/castellan-5.7.0/AUTHORS new/castellan-5.8.0/AUTHORS
--- old/castellan-5.7.0/AUTHORS 2026-05-18 11:03:55.000000000 +0200
+++ new/castellan-5.8.0/AUTHORS 2026-07-10 15:40:53.000000000 +0200
@@ -1,3 +1,4 @@
+Adam Harwell <[email protected]>
 Ade Lee <[email protected]>
 Akihiro Motoki <[email protected]>
 Alan Bishop <[email protected]>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/castellan-5.7.0/ChangeLog 
new/castellan-5.8.0/ChangeLog
--- old/castellan-5.7.0/ChangeLog       2026-05-18 11:03:55.000000000 +0200
+++ new/castellan-5.8.0/ChangeLog       2026-07-10 15:40:53.000000000 +0200
@@ -1,6 +1,17 @@
 CHANGES
 =======
 
+5.8.0
+-----
+
+* Drop Python 3.10 support
+* zuul: Use openstack-python3-next-jobs template
+* pre-commit: Bump versions
+* Do not install code to build release notes
+* vault: Apply auth\_path for approle auth method
+* Add token-based auth method to VaultKeyManager
+* Do not expose internal client exceptions
+
 5.7.0
 -----
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/castellan-5.7.0/PKG-INFO new/castellan-5.8.0/PKG-INFO
--- old/castellan-5.7.0/PKG-INFO        2026-05-18 11:03:55.745335300 +0200
+++ new/castellan-5.8.0/PKG-INFO        2026-07-10 15:40:53.325220300 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: castellan
-Version: 5.7.0
+Version: 5.8.0
 Summary: Generic Key Manager interface for OpenStack
 Author-email: OpenStack <[email protected]>
 License: Apache-2.0
@@ -13,12 +13,12 @@
 Classifier: Operating System :: POSIX :: Linux
 Classifier: Programming Language :: Python
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
 Classifier: Typing :: Typed
-Requires-Python: >=3.10
+Requires-Python: >=3.11
 Description-Content-Type: text/x-rst
 License-File: LICENSE
 Requires-Dist: pbr>=2.0.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/castellan-5.7.0/castellan/_config_driver.py 
new/castellan-5.8.0/castellan/_config_driver.py
--- old/castellan-5.7.0/castellan/_config_driver.py     2026-05-18 
11:02:59.000000000 +0200
+++ new/castellan-5.8.0/castellan/_config_driver.py     2026-07-10 
15:40:18.000000000 +0200
@@ -60,9 +60,7 @@
 LOG = log.getLogger(__name__)
 
 
-class CastellanConfigurationSourceDriver(
-    sources.ConfigurationSourceDriver  # type: ignore[misc]
-):
+class CastellanConfigurationSourceDriver(sources.ConfigurationSourceDriver):
     """A backend driver for configuration values served through castellan.
 
     Required options:
@@ -76,7 +74,7 @@
                       will be used to fetch the values through castellan.
     """
 
-    _castellan_driver_opts = [
+    _castellan_driver_opts: list[cfg.Opt] = [
         cfg.StrOpt(
             'config_file',
             required=True,
@@ -106,9 +104,7 @@
         )
 
 
-class CastellanConfigurationSource(
-    sources.ConfigurationSource  # type: ignore[misc]
-):
+class CastellanConfigurationSource(sources.ConfigurationSource):
     """A configuration source for values served through castellan.
 
     :param config_file: The path to a castellan configuration file.
@@ -146,7 +142,7 @@
         cfg.ConfigParser(mapping_file, self._mapping).parse()
 
     def get(
-        self, group_name: str, option_name: str, opt: cfg.Opt
+        self, group_name: str | None, option_name: str, opt: cfg.Opt
     ) -> tuple[Any, cfg.LocationInfo | None]:
         try:
             group_name = group_name or "DEFAULT"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/castellan-5.7.0/castellan/key_manager/barbican_key_manager.py 
new/castellan-5.8.0/castellan/key_manager/barbican_key_manager.py
--- old/castellan-5.7.0/castellan/key_manager/barbican_key_manager.py   
2026-05-18 11:02:59.000000000 +0200
+++ new/castellan-5.8.0/castellan/key_manager/barbican_key_manager.py   
2026-07-10 15:40:18.000000000 +0200
@@ -360,9 +360,8 @@
             self._delete_order(barbican_client, order_ref)
             return secret_ref
         except (
-            barbican_exceptions.HTTPAuthError,
-            barbican_exceptions.HTTPClientError,
-            barbican_exceptions.HTTPServerError,
+            barbican_exceptions.BarbicanException,
+            barbican_exceptions.HTTPError,
         ) as e:
             LOG.error("Error creating key: %s", e)
             raise exception.KeyManagerError(reason=e)
@@ -411,9 +410,8 @@
             self._delete_order(barbican_client, order_ref)
             return private_key_uuid, public_key_uuid
         except (
-            barbican_exceptions.HTTPAuthError,
-            barbican_exceptions.HTTPClientError,
-            barbican_exceptions.HTTPServerError,
+            barbican_exceptions.BarbicanException,
+            barbican_exceptions.HTTPError,
         ) as e:
             LOG.error("Error creating key pair: %s", e)
             raise exception.KeyManagerError(reason=e)
@@ -504,9 +502,8 @@
             secret_ref = secret.store()
             return self._retrieve_secret_uuid(secret_ref)
         except (
-            barbican_exceptions.HTTPAuthError,
-            barbican_exceptions.HTTPClientError,
-            barbican_exceptions.HTTPServerError,
+            barbican_exceptions.BarbicanException,
+            barbican_exceptions.HTTPError,
         ) as e:
             LOG.error("Error storing object: %s", e)
             raise exception.KeyManagerError(reason=e)
@@ -698,16 +695,13 @@
         try:
             return barbican_client.secrets.get(object_id)
         except (
-            barbican_exceptions.HTTPAuthError,
-            barbican_exceptions.HTTPClientError,
-            barbican_exceptions.HTTPServerError,
+            barbican_exceptions.BarbicanException,
+            barbican_exceptions.HTTPError,
         ) as e:
             with excutils.save_and_reraise_exception():
                 LOG.error("Error getting secret metadata: %s", e)
 
-    def _is_secret_not_found_error(
-        self, error: barbican_exceptions.HTTPClientError
-    ) -> bool:
+    def _is_secret_not_found_error(self, error: Exception) -> bool:
         if (
             isinstance(error, barbican_exceptions.HTTPClientError)
             and error.status_code == 404
@@ -736,9 +730,8 @@
             secret = self._get_secret(context, managed_object_id)
             return self._get_castellan_object(secret, metadata_only)
         except (
-            barbican_exceptions.HTTPAuthError,
-            barbican_exceptions.HTTPClientError,
-            barbican_exceptions.HTTPServerError,
+            barbican_exceptions.BarbicanException,
+            barbican_exceptions.HTTPError,
         ) as e:
             LOG.error("Error retrieving object: %s", e)
             if self._is_secret_not_found_error(e):
@@ -761,8 +754,6 @@
         :param managed_object_id: the UUID of the object to delete
         :param force: specifies if the secret must be deleted even when they
             have consumers.
-        :raises ValueError: if the secret has consumers but no force parameter
-            is provided or if force equals False.
         :raises KeyManagerError: if object deletion fails
         :raises ManagedObjectNotFoundError: if the object could not be found
         """
@@ -773,9 +764,8 @@
         try:
             barbican_client.secrets.delete(managed_object_id, force)
         except (
-            barbican_exceptions.HTTPAuthError,
-            barbican_exceptions.HTTPClientError,
-            barbican_exceptions.HTTPServerError,
+            barbican_exceptions.BarbicanException,
+            barbican_exceptions.HTTPError,
         ) as e:
             LOG.error("Error deleting object: %s", e)
             if self._is_secret_not_found_error(e):
@@ -811,9 +801,8 @@
             )
 
         except (
-            barbican_exceptions.HTTPAuthError,
-            barbican_exceptions.HTTPClientError,
-            barbican_exceptions.HTTPServerError,
+            barbican_exceptions.BarbicanException,
+            barbican_exceptions.HTTPError,
         ) as e:
             LOG.error("Error adding consumer: %s", e)
             if self._is_secret_not_found_error(e):
@@ -835,9 +824,8 @@
                 managed_object_id, **consumer_data
             )
         except (
-            barbican_exceptions.HTTPAuthError,
-            barbican_exceptions.HTTPClientError,
-            barbican_exceptions.HTTPServerError,
+            barbican_exceptions.BarbicanException,
+            barbican_exceptions.HTTPError,
         ) as e:
             LOG.error("Error removing consumer: %s", e)
             if self._is_secret_not_found_error(e):
@@ -876,9 +864,8 @@
         try:
             secrets = barbican_client.secrets.list(secret_type=secret_type)
         except (
-            barbican_exceptions.HTTPAuthError,
-            barbican_exceptions.HTTPClientError,
-            barbican_exceptions.HTTPServerError,
+            barbican_exceptions.BarbicanException,
+            barbican_exceptions.HTTPError,
         ) as e:
             LOG.error("Error listing objects: %s", e)
             raise exception.KeyManagerError(reason=e)
@@ -888,9 +875,8 @@
                 obj = self._get_castellan_object(secret, metadata_only)
                 objects.append(obj)
             except (
-                barbican_exceptions.HTTPAuthError,
-                barbican_exceptions.HTTPClientError,
-                barbican_exceptions.HTTPServerError,
+                barbican_exceptions.BarbicanException,
+                barbican_exceptions.HTTPError,
             ) as e:
                 LOG.warning(
                     "Error occurred while retrieving object "
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/castellan-5.7.0/castellan/key_manager/vault_key_manager.py 
new/castellan-5.8.0/castellan/key_manager/vault_key_manager.py
--- old/castellan-5.7.0/castellan/key_manager/vault_key_manager.py      
2026-05-18 11:02:59.000000000 +0200
+++ new/castellan-5.8.0/castellan/key_manager/vault_key_manager.py      
2026-07-10 15:40:18.000000000 +0200
@@ -53,6 +53,14 @@
 _vault_opts = [
     cfg.StrOpt('root_token_id', secret=True, help='root token for vault'),
     cfg.StrOpt(
+        'auth_method',
+        default='approle',
+        choices=('approle', 'jwt', 'kubernetes'),
+        help='Auth method to use when connecting to Vault. '
+        '"approle" uses approle_role_id and approle_secret_id. '
+        '"jwt" and "kubernetes" use token_role and token_file.',
+    ),
+    cfg.StrOpt(
         'approle_role_id',
         secret=True,
         help='AppRole role_id for authentication with vault',
@@ -63,6 +71,26 @@
         help='AppRole secret_id for authentication with vault',
     ),
     cfg.StrOpt(
+        'token_role',
+        help='Vault role name for token-based auth. '
+        'Required when auth_method is jwt or kubernetes.',
+    ),
+    cfg.StrOpt(
+        'token_file',
+        help='Path to the token file used for Vault login. '
+        'Required when auth_method is jwt or kubernetes.',
+    ),
+    cfg.StrOpt(
+        'auth_path',
+        help='Mount path of the Vault auth backend, used in '
+        'the login URL /v1/auth/<auth_path>/login. '
+        'Defaults to the value of auth_method when not set. '
+        'Override this when the auth backend is mounted at '
+        'a non-default path '
+        '(e.g. "kubernetes-my-cluster" instead of '
+        '"kubernetes").',
+    ),
+    cfg.StrOpt(
         'kv_mountpoint',
         default=_DEFAULT_MOUNTPOINT,
         help='Mountpoint of KV store in Vault to use',
@@ -79,7 +107,7 @@
     cfg.URIOpt(
         'vault_url',
         default=_DEFAULT_VAULT_URL,
-        schemes=('http', 'https'),
+        schemes=['http', 'https'],
         help='Use this endpoint to connect to Vault',
     ),
     cfg.StrOpt('ssl_ca_crt_file', help='Absolute path to ca cert file'),
@@ -126,9 +154,13 @@
     _root_token_id: str | None
     _approle_role_id: str | None
     _approle_secret_id: str | None
-    _cached_approle_token_id: str | None
-    _approle_token_ttl: int | None
-    _approle_token_issue: datetime.datetime | None
+    _auth_method: str
+    _token_role: str | None
+    _token_file: str
+    _auth_path: str
+    _cached_token_id: str | None
+    _cached_token_ttl: int | None
+    _cached_token_issue: datetime.datetime | None
     _kv_mountpoint: str
     _kv_path: str | None
     _kv_version: int
@@ -144,9 +176,13 @@
         self._root_token_id = self._conf.vault.root_token_id
         self._approle_role_id = self._conf.vault.approle_role_id
         self._approle_secret_id = self._conf.vault.approle_secret_id
-        self._cached_approle_token_id = None
-        self._approle_token_ttl = None
-        self._approle_token_issue = None
+        self._auth_method = self._conf.vault.auth_method
+        self._token_role = self._conf.vault.token_role
+        self._token_file = self._conf.vault.token_file
+        self._auth_path = self._conf.vault.auth_path or self._auth_method
+        self._cached_token_id = None
+        self._cached_token_ttl = None
+        self._cached_token_issue = None
         self._kv_mountpoint = self._conf.vault.kv_mountpoint
         self._kv_path = self._conf.vault.kv_path
         self._kv_version = self._conf.vault.kv_version
@@ -178,16 +214,16 @@
         )
 
     @property
-    def _approle_token_id(self) -> str | None:
+    def _cached_token(self) -> str | None:
         if (
-            self._approle_token_issue is not None
-            and self._approle_token_ttl is not None
+            self._cached_token_issue is not None
+            and self._cached_token_ttl is not None
             and timeutils.is_older_than(
-                self._approle_token_issue, self._approle_token_ttl
+                self._cached_token_issue, self._cached_token_ttl
             )
         ):
-            self._cached_approle_token_id = None
-        return self._cached_approle_token_id
+            self._cached_token_id = None
+        return self._cached_token_id
 
     def _set_namespace(self, headers: dict[str, str]) -> dict[str, str]:
         if self._namespace:
@@ -198,47 +234,84 @@
         if self._root_token_id:
             return self._set_namespace({'X-Vault-Token': self._root_token_id})
 
-        if self._approle_token_id:
-            return self._set_namespace(
-                {'X-Vault-Token': self._approle_token_id}
-            )
+        if self._cached_token:
+            return self._set_namespace({'X-Vault-Token': self._cached_token})
 
-        if self._approle_role_id:
+        login_url = f'{self._get_url()}v1/auth/{self._auth_path}/login'
+
+        if self._auth_method == 'approle':
+            if not self._approle_role_id:
+                return {}
             params: dict[str, str] = {'role_id': self._approle_role_id}
             if self._approle_secret_id:
                 params['secret_id'] = self._approle_secret_id
-            approle_login_url = f'{self._get_url()}v1/auth/approle/login'
-            token_issue_utc = timeutils.utcnow()
-            headers = self._set_namespace({})
+            return self._vault_login(login_url, params)
+
+        elif self._auth_method in ('jwt', 'kubernetes'):
+            if not self._token_role:
+                raise exception.KeyManagerError(
+                    "token_role is required when "
+                    f"auth_method={self._auth_method}"
+                )
+            if not self._token_file:
+                raise exception.KeyManagerError(
+                    "token_file is required when "
+                    f"auth_method={self._auth_method}"
+                )
             try:
-                resp = self._session.post(
-                    url=approle_login_url,
-                    json=params,
-                    headers=headers,
-                    verify=self._verify_server,
-                    timeout=self._timeout,
+                with open(self._token_file) as f:
+                    token = f.read().strip()
+            except OSError as ex:
+                raise exception.KeyManagerError(
+                    f"Failed to read token file from {self._token_file}: {ex}"
                 )
-            except Exception as ex:
-                raise exception.KeyManagerError(str(ex))
 
-            if resp.status_code in _EXCEPTIONS_BY_CODE:
-                raise exception.KeyManagerError(resp.reason)
-            if resp.status_code == requests.codes['forbidden']:
-                raise exception.Forbidden()
-
-            resp_data = resp.json()
-
-            if resp.status_code == requests.codes['bad_request']:
-                raise exception.KeyManagerError(', '.join(resp_data['errors']))
-
-            self._cached_approle_token_id = resp_data['auth']['client_token']
-            self._approle_token_issue = token_issue_utc
-            self._approle_token_ttl = resp_data['auth']['lease_duration']
-            return self._set_namespace(
-                {'X-Vault-Token': self._cached_approle_token_id}
+            params = {
+                'role': self._token_role,
+                'jwt': token,
+            }
+            return self._vault_login(login_url, params)
+
+        raise exception.KeyManagerError(
+            f"Unknown auth_method: {self._auth_method}"
+        )
+
+    def _vault_login(
+        self,
+        login_url: str,
+        params: dict[str, str],
+    ) -> dict[str, str]:
+        """Authenticate with Vault and cache the resulting token.
+
+        Shared by AppRole and token-based (Kubernetes, JWT/OIDC) auth methods.
+        """
+        token_issue_utc = timeutils.utcnow()
+        headers = self._set_namespace({})
+        try:
+            resp = self._session.post(
+                url=login_url,
+                json=params,
+                headers=headers,
+                verify=self._verify_server,
+                timeout=self._timeout,
             )
+        except Exception as ex:
+            raise exception.KeyManagerError(str(ex))
 
-        return {}
+        if resp.status_code in _EXCEPTIONS_BY_CODE:
+            raise exception.KeyManagerError(resp.reason)
+        if resp.status_code == requests.codes['forbidden']:
+            raise exception.Forbidden()
+
+        resp_data = resp.json()
+
+        if resp.status_code == requests.codes['bad_request']:
+            raise exception.KeyManagerError(', '.join(resp_data['errors']))
+
+        self._cached_token_id = resp_data['auth']['client_token']
+        self._cached_token_issue = token_issue_utc
+        self._cached_token_ttl = resp_data['auth']['lease_duration']
+        return self._set_namespace({'X-Vault-Token': self._cached_token_id})
 
     def _do_http_request(
         self,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/castellan-5.7.0/castellan/tests/functional/config.py 
new/castellan-5.8.0/castellan/tests/functional/config.py
--- old/castellan-5.7.0/castellan/tests/functional/config.py    2026-05-18 
11:02:59.000000000 +0200
+++ new/castellan-5.8.0/castellan/tests/functional/config.py    2026-07-10 
15:40:18.000000000 +0200
@@ -68,7 +68,7 @@
         config_to_load.append(main_config)
 
     TEST_CONF(
-        (),  # Required to load an anonymous configuration
+        [],  # Required to load an anonymous configuration
         default_config_files=config_to_load,
     )
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/castellan-5.7.0/castellan/tests/functional/key_manager/test_barbican_key_manager.py
 
new/castellan-5.8.0/castellan/tests/functional/key_manager/test_barbican_key_manager.py
--- 
old/castellan-5.7.0/castellan/tests/functional/key_manager/test_barbican_key_manager.py
     2026-05-18 11:02:59.000000000 +0200
+++ 
new/castellan-5.8.0/castellan/tests/functional/key_manager/test_barbican_key_manager.py
     2026-07-10 15:40:18.000000000 +0200
@@ -418,13 +418,11 @@
         }
         self.key_mgr.add_consumer(self.ctxt, stored_id, consumer)
 
-        e = self.assertRaises(
-            ValueError, self.key_mgr.delete, self.ctxt, stored_id
-        )
-        self.assertIn(
-            "Secret has consumers! Remove them first or use the "
-            "force parameter to delete it.",
-            str(e),
+        self.assertRaises(
+            exception.KeyManagerError,
+            self.key_mgr.delete,
+            self.ctxt,
+            stored_id,
         )
 
     def test_consumer_add_secret_delete_force_parameter_false(self):
@@ -447,13 +445,12 @@
         }
         self.key_mgr.add_consumer(self.ctxt, stored_id, consumer)
 
-        e = self.assertRaises(
-            ValueError, self.key_mgr.delete, self.ctxt, stored_id, False
-        )
-        self.assertIn(
-            "Secret has consumers! Remove them first or use the "
-            "force parameter to delete it.",
-            str(e),
+        self.assertRaises(
+            exception.KeyManagerError,
+            self.key_mgr.delete,
+            self.ctxt,
+            stored_id,
+            False,
         )
 
     def test_consumer_add_secret_delete_force_parameter_true(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/castellan-5.7.0/castellan/tests/functional/key_manager/test_vault_key_manager.py
 
new/castellan-5.8.0/castellan/tests/functional/key_manager/test_vault_key_manager.py
--- 
old/castellan-5.7.0/castellan/tests/functional/key_manager/test_vault_key_manager.py
        2026-05-18 11:02:59.000000000 +0200
+++ 
new/castellan-5.8.0/castellan/tests/functional/key_manager/test_vault_key_manager.py
        2026-07-10 15:40:18.000000000 +0200
@@ -17,6 +17,7 @@
 """
 
 import os
+from typing import Any
 import unittest
 import uuid
 
@@ -130,7 +131,7 @@
     def _mount_kv(self, vault_mountpoint):
         backends = self.session.get(f'{self.vault_url}/v1/sys/mounts').json()
         if vault_mountpoint not in backends:
-            params = {
+            params: dict[str, Any] = {
                 'type': 'kv',
                 'options': {
                     'version': 2,
@@ -142,7 +143,7 @@
             )
 
     def _enable_approle(self):
-        params = {'type': 'approle'}
+        params: dict[str, Any] = {'type': 'approle'}
         self.session.post(
             '{}/{}'.format(
                 self.vault_url, AUTH_ENDPOINT.format(auth_type='approle')
@@ -151,7 +152,7 @@
         )
 
     def _create_policy(self, vault_policy):
-        params = {
+        params: dict[str, Any] = {
             'rules': TEST_POLICY.format(backend=self.mountpoint),
         }
         self.session.put(
@@ -160,7 +161,7 @@
         )
 
     def _create_approle(self, vault_approle, vault_policy):
-        params = {
+        params: dict[str, Any] = {
             'token_ttl': '60s',
             'token_max_ttl': '60s',
             'policies': [vault_policy],
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/castellan-5.7.0/castellan/tests/unit/key_manager/test_vault_key_manager.py 
new/castellan-5.8.0/castellan/tests/unit/key_manager/test_vault_key_manager.py
--- 
old/castellan-5.7.0/castellan/tests/unit/key_manager/test_vault_key_manager.py  
    2026-05-18 11:02:59.000000000 +0200
+++ 
new/castellan-5.8.0/castellan/tests/unit/key_manager/test_vault_key_manager.py  
    2026-07-10 15:40:18.000000000 +0200
@@ -17,8 +17,12 @@
 Test cases for Vault key manager.
 """
 
+import os
+import tempfile
+
 import requests_mock
 
+from castellan.common import exception
 from castellan.key_manager import vault_key_manager
 from castellan.tests.unit.key_manager import test_key_manager
 
@@ -27,6 +31,31 @@
     def _create_key_manager(self):
         return vault_key_manager.VaultKeyManager(self.conf)
 
+    def _create_sa_token_file(self, content="fake-sa-jwt-token"):
+        """Create a temporary SA token file and register cleanup."""
+        token_file = tempfile.NamedTemporaryFile(
+            mode='w',
+            suffix='.token',
+            delete=False,
+        )
+        token_file.write(content)
+        token_file.close()
+        self.addCleanup(os.unlink, token_file.name)
+        return token_file.name
+
+    def _setup_token_auth(
+        self,
+        auth_method="kubernetes",
+        role="my-role",
+        token_content="fake-sa-jwt-token",
+        auth_path=None,
+    ):
+        """Configure the key manager for token-based auth."""
+        self.key_mgr._auth_method = auth_method
+        self.key_mgr._token_role = role
+        self.key_mgr._token_file = self._create_sa_token_file(token_content)
+        self.key_mgr._auth_path = auth_path or auth_method
+
     def test_auth_headers_root_token(self):
         self.key_mgr._root_token_id = "spam"
         expected_headers = {"X-Vault-Token": "spam"}
@@ -66,3 +95,184 @@
             "X-Vault-Namespace": "ham",
         }
         self.assertEqual(expected_headers, self.key_mgr._build_auth_headers())
+
+    @requests_mock.Mocker()
+    def test_auth_headers_app_role_token_caching(self, m):
+        """Verify that the AppRole token is cached and reused."""
+        self.key_mgr._approle_role_id = "spam"
+        self.key_mgr._approle_secret_id = "secret"
+        m.post(
+            "http://127.0.0.1:8200/v1/auth/approle/login";,
+            json={"auth": {"client_token": "token", "lease_duration": 3600}},
+        )
+        headers1 = self.key_mgr._build_auth_headers()
+        headers2 = self.key_mgr._build_auth_headers()
+        self.assertEqual(headers1, headers2)
+        self.assertEqual({"X-Vault-Token": "token"}, headers2)
+        self.assertEqual(1, m.call_count)
+
+    @requests_mock.Mocker()
+    def test_auth_headers_token_file(self, m):
+        self._setup_token_auth()
+        m.post(
+            "http://127.0.0.1:8200/v1/auth/kubernetes/login";,
+            json={
+                "auth": {
+                    "client_token": "k8s-token",
+                    "lease_duration": 1800,
+                },
+            },
+        )
+        expected_headers = {"X-Vault-Token": "k8s-token"}
+        self.assertEqual(expected_headers, self.key_mgr._build_auth_headers())
+        # Verify the request body
+        self.assertEqual(
+            m.last_request.json(),
+            {"role": "my-role", "jwt": "fake-sa-jwt-token"},
+        )
+
+    @requests_mock.Mocker()
+    def test_auth_headers_token_file_custom_path(self, m):
+        self._setup_token_auth(auth_method="jwt")
+        m.post(
+            "http://127.0.0.1:8200/v1/auth/jwt/login";,
+            json={
+                "auth": {
+                    "client_token": "jwt-token",
+                    "lease_duration": 900,
+                },
+            },
+        )
+        expected_headers = {"X-Vault-Token": "jwt-token"}
+        self.assertEqual(expected_headers, self.key_mgr._build_auth_headers())
+
+    @requests_mock.Mocker()
+    def test_auth_headers_token_file_with_namespace(self, m):
+        self._setup_token_auth()
+        self.key_mgr._namespace = "my-namespace"
+        m.post(
+            "http://127.0.0.1:8200/v1/auth/kubernetes/login";,
+            json={
+                "auth": {
+                    "client_token": "k8s-ns-token",
+                    "lease_duration": 1800,
+                },
+            },
+        )
+        expected_headers = {
+            "X-Vault-Token": "k8s-ns-token",
+            "X-Vault-Namespace": "my-namespace",
+        }
+        self.assertEqual(expected_headers, self.key_mgr._build_auth_headers())
+
+    def test_auth_headers_token_file_missing(self):
+        self.key_mgr._auth_method = 'kubernetes'
+        self.key_mgr._token_role = "my-role"
+        self.key_mgr._auth_path = "kubernetes"
+        self.key_mgr._token_file = "/nonexistent/path/token"
+        self.assertRaises(
+            exception.KeyManagerError, self.key_mgr._build_auth_headers
+        )
+
+    @requests_mock.Mocker()
+    def test_auth_headers_token_file_forbidden(self, m):
+        self._setup_token_auth(role="bad-role")
+        m.post(
+            "http://127.0.0.1:8200/v1/auth/kubernetes/login";,
+            status_code=403,
+        )
+        self.assertRaises(
+            exception.Forbidden, self.key_mgr._build_auth_headers
+        )
+
+    @requests_mock.Mocker()
+    def test_auth_headers_token_file_bad_request(self, m):
+        self._setup_token_auth()
+        m.post(
+            "http://127.0.0.1:8200/v1/auth/kubernetes/login";,
+            status_code=400,
+            json={"errors": ["missing role", "invalid jwt"]},
+        )
+        self.assertRaises(
+            exception.KeyManagerError, self.key_mgr._build_auth_headers
+        )
+
+    @requests_mock.Mocker()
+    def test_auth_headers_token_file_caching(self, m):
+        """Verify that the token from file-based auth is cached and reused."""
+        self._setup_token_auth()
+        m.post(
+            "http://127.0.0.1:8200/v1/auth/kubernetes/login";,
+            json={
+                "auth": {
+                    "client_token": "cached-token",
+                    "lease_duration": 3600,
+                },
+            },
+        )
+        # First call -- authenticates
+        headers1 = self.key_mgr._build_auth_headers()
+        # Second call -- should use cached token
+        headers2 = self.key_mgr._build_auth_headers()
+        self.assertEqual(headers1, headers2)
+        self.assertEqual({"X-Vault-Token": "cached-token"}, headers2)
+        # Only one HTTP request should have been made
+        self.assertEqual(1, m.call_count)
+
+    def test_auth_priority_root_over_token_file(self):
+        """Root token takes priority over token file auth."""
+        self.key_mgr._root_token_id = "root-token"
+        self.key_mgr._auth_method = 'kubernetes'
+        self.key_mgr._token_role = "my-role"
+        expected_headers = {"X-Vault-Token": "root-token"}
+        self.assertEqual(expected_headers, self.key_mgr._build_auth_headers())
+
+    @requests_mock.Mocker()
+    def test_auth_priority_root_over_approle(self, m):
+        """Root token takes priority over AppRole auth."""
+        self.key_mgr._root_token_id = "root-token"
+        self.key_mgr._approle_role_id = "app-role"
+        expected_headers = {"X-Vault-Token": "root-token"}
+        self.assertEqual(expected_headers, self.key_mgr._build_auth_headers())
+
+    def test_auth_token_missing_role(self):
+        """auth_method=kubernetes without token_role raises error."""
+        self.key_mgr._auth_method = 'kubernetes'
+        self.key_mgr._auth_path = 'kubernetes'
+        self.assertRaises(
+            exception.KeyManagerError, self.key_mgr._build_auth_headers
+        )
+
+    @requests_mock.Mocker()
+    def test_auth_headers_token_file_custom_auth_path(self, m):
+        """auth_path overrides auth_method in the login URL."""
+        self._setup_token_auth(
+            auth_method="kubernetes",
+            auth_path="kubernetes-my-custom-cluster",
+        )
+        m.post(
+            "http://127.0.0.1:8200/v1/auth/kubernetes-my-custom-cluster/login";,
+            json={
+                "auth": {
+                    "client_token": "custom-token",
+                    "lease_duration": 1800,
+                },
+            },
+        )
+        expected_headers = {"X-Vault-Token": "custom-token"}
+        self.assertEqual(expected_headers, self.key_mgr._build_auth_headers())
+
+    def test_auth_token_missing_token_file(self):
+        """auth_method=kubernetes without token_file raises error."""
+        self.key_mgr._auth_method = 'kubernetes'
+        self.key_mgr._auth_path = 'kubernetes'
+        self.key_mgr._token_role = "my-role"
+        self.key_mgr._token_file = None
+        self.assertRaises(
+            exception.KeyManagerError, self.key_mgr._build_auth_headers
+        )
+
+    def test_auth_approle_no_role_id_returns_empty(self):
+        """auth_method=approle without approle_role_id returns empty."""
+        self.key_mgr._auth_method = 'approle'
+        self.assertEqual({}, self.key_mgr._build_auth_headers())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/castellan-5.7.0/castellan.egg-info/PKG-INFO 
new/castellan-5.8.0/castellan.egg-info/PKG-INFO
--- old/castellan-5.7.0/castellan.egg-info/PKG-INFO     2026-05-18 
11:03:55.000000000 +0200
+++ new/castellan-5.8.0/castellan.egg-info/PKG-INFO     2026-07-10 
15:40:53.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: castellan
-Version: 5.7.0
+Version: 5.8.0
 Summary: Generic Key Manager interface for OpenStack
 Author-email: OpenStack <[email protected]>
 License: Apache-2.0
@@ -13,12 +13,12 @@
 Classifier: Operating System :: POSIX :: Linux
 Classifier: Programming Language :: Python
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
 Classifier: Typing :: Typed
-Requires-Python: >=3.10
+Requires-Python: >=3.11
 Description-Content-Type: text/x-rst
 License-File: LICENSE
 Requires-Dist: pbr>=2.0.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/castellan-5.7.0/castellan.egg-info/SOURCES.txt 
new/castellan-5.8.0/castellan.egg-info/SOURCES.txt
--- old/castellan-5.7.0/castellan.egg-info/SOURCES.txt  2026-05-18 
11:03:55.000000000 +0200
+++ new/castellan-5.8.0/castellan.egg-info/SOURCES.txt  2026-07-10 
15:40:53.000000000 +0200
@@ -111,6 +111,7 @@
 releasenotes/notes/deprecate-vault-use_ssl-64a3b006bdcb1303.yaml
 releasenotes/notes/deprecate-version-attribute-2a8ea36008bc8b94.yaml
 releasenotes/notes/drop-python-2-7-73d3113c69d724d6.yaml
+releasenotes/notes/drop-python-3.10-55259b98a591d862.yaml
 releasenotes/notes/fix-vault-create-key-b4340a3067cbd93c.yaml
 releasenotes/notes/fix-vault-flaky-kv-api-version-b0cd9d62a39d2907.yaml
 releasenotes/notes/implements-keymanager-option-discovery-13a46c1dfc036a3f.yaml
@@ -122,6 +123,7 @@
 
releasenotes/notes/use-barbican-endpoint-type-config-option-e583d30930cc22ba.yaml
 releasenotes/notes/use-barbican-region-name-config-option-31bec809292302b8.yaml
 releasenotes/notes/vault-approle-support-5ea04daea07a152f.yaml
+releasenotes/notes/vault-kubernetes-auth-a7b3c5d9e1f24680.yaml
 releasenotes/notes/vault-kv-mountpoint-919eb547764a0c74.yaml
 releasenotes/notes/vault-namespaces-7d334e7407396df9.yaml
 releasenotes/notes/vault-timeout-5eebb432e0943f74.yaml
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/castellan-5.7.0/castellan.egg-info/pbr.json 
new/castellan-5.8.0/castellan.egg-info/pbr.json
--- old/castellan-5.7.0/castellan.egg-info/pbr.json     2026-05-18 
11:03:55.000000000 +0200
+++ new/castellan-5.8.0/castellan.egg-info/pbr.json     2026-07-10 
15:40:53.000000000 +0200
@@ -1 +1 @@
-{"git_version": "7dd95d3", "is_release": true}
\ No newline at end of file
+{"git_version": "5405734", "is_release": true}
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/castellan-5.7.0/pyproject.toml 
new/castellan-5.8.0/pyproject.toml
--- old/castellan-5.7.0/pyproject.toml  2026-05-18 11:02:59.000000000 +0200
+++ new/castellan-5.8.0/pyproject.toml  2026-07-10 15:40:18.000000000 +0200
@@ -11,7 +11,7 @@
 readme = {file = "README.rst", content-type = "text/x-rst"}
 license = {text = "Apache-2.0"}
 dynamic = ["version", "dependencies"]
-requires-python = ">=3.10"
+requires-python = ">=3.11"
 classifiers = [
   "Development Status :: 5 - Production/Stable",
   "Environment :: Console",
@@ -20,10 +20,10 @@
   "Operating System :: POSIX :: Linux",
   "Programming Language :: Python",
   "Programming Language :: Python :: 3",
-  "Programming Language :: Python :: 3.10",
   "Programming Language :: Python :: 3.11",
   "Programming Language :: Python :: 3.12",
   "Programming Language :: Python :: 3.13",
+  "Programming Language :: Python :: 3.14",
   "Typing :: Typed",
 ]
 
@@ -66,7 +66,7 @@
 "castellan/tests/*" = ["S"]
 
 [tool.mypy]
-python_version = "3.10"
+python_version = "3.11"
 show_column_numbers = true
 show_error_context = true
 strict = true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/castellan-5.7.0/releasenotes/notes/drop-python-3.10-55259b98a591d862.yaml 
new/castellan-5.8.0/releasenotes/notes/drop-python-3.10-55259b98a591d862.yaml
--- 
old/castellan-5.7.0/releasenotes/notes/drop-python-3.10-55259b98a591d862.yaml   
    1970-01-01 01:00:00.000000000 +0100
+++ 
new/castellan-5.8.0/releasenotes/notes/drop-python-3.10-55259b98a591d862.yaml   
    2026-07-10 15:40:18.000000000 +0200
@@ -0,0 +1,5 @@
+---
+upgrade:
+  - |
+    Support for Python 3.10 has been dropped. The minimum version of Python now
+    supported is 3.11.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/castellan-5.7.0/releasenotes/notes/vault-kubernetes-auth-a7b3c5d9e1f24680.yaml
 
new/castellan-5.8.0/releasenotes/notes/vault-kubernetes-auth-a7b3c5d9e1f24680.yaml
--- 
old/castellan-5.7.0/releasenotes/notes/vault-kubernetes-auth-a7b3c5d9e1f24680.yaml
  1970-01-01 01:00:00.000000000 +0100
+++ 
new/castellan-5.8.0/releasenotes/notes/vault-kubernetes-auth-a7b3c5d9e1f24680.yaml
  2026-07-10 15:40:18.000000000 +0200
@@ -0,0 +1,15 @@
+---
+features:
+  - |
+    Added token-based auth method support to the Vault key manager.
+    A new ``[vault] auth_method`` option (choices: ``approle``,
+    ``jwt`` and ``kubernetes``; default: ``approle``) controls how
+    castellan authenticates with Vault. The ``jwt`` and
+    ``kubernetes`` methods read a JWT from a file and present it
+    to a Vault auth backend. An optional ``auth_path`` option
+    allows overriding the auth backend mount path when it differs
+    from the default (e.g. ``kubernetes-my-cluster`` instead of
+    ``kubernetes``). New ``[vault]`` config options:
+    ``auth_method``, ``token_role``, ``token_file``, and
+    ``auth_path``. The ``root_token_id`` option continues to take
+    priority over any configured auth method when set.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/castellan-5.7.0/tox.ini new/castellan-5.8.0/tox.ini
--- old/castellan-5.7.0/tox.ini 2026-05-18 11:02:59.000000000 +0200
+++ new/castellan-5.8.0/tox.ini 2026-07-10 15:40:18.000000000 +0200
@@ -9,7 +9,6 @@
 constraints =
   
{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
 deps =
-  -r{toxinidir}/requirements.txt
   -r{toxinidir}/test-requirements.txt
 commands = stestr run --slowest --concurrency=1 {posargs}
 
@@ -71,6 +70,7 @@
   make -C doc/build/pdf
 
 [testenv:releasenotes]
+skip_install = true
 deps = {[testenv:docs]deps}
 commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html 
releasenotes/source releasenotes/build/html
 
@@ -108,6 +108,7 @@
   typing
 
 [testenv:bindep]
+skip_install = true
 # Do not install any requirements. We want this to be fast and work even if
 # system dependencies are missing, since it's used to tell you what system
 # dependencies are missing! This also means that bindep must be installed

Reply via email to