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