Client contract (rewrite pack) — python-barbicanclient — branch stable/2026.2
Branch
stable/2026.2@8162006e8677— repository https://opendev.org/openstack/python-barbicanclient — analysed on 2026-11-01
This pack serves two purposes:
- Keep this client working against a re-implemented key manager, by stating what it requires from the service.
- Re-implement the commands, with the machine-readable catalogue.
No replay against the real Barbican was done. The client ran against a local stub only. A stub answering what it is told cannot show that a service conforms: see § 5.
1. Contents of _rewrite/
| File | Content | Use |
|---|---|---|
[cli-commands_stable-2026.2.json](_rewrite/cli-commands_stable-2026.2.json) | the 23 commands: class, purpose, each of the 78 arguments (names, type, default, choices, required, exclusive groups, help), output kind | generate a parser; compare argument names |
[client-contract_stable-2026.2.json](_rewrite/client-contract_stable-2026.2.json) | per command: observed requests, static requests with file:line and condition, exit codes on the stub; discovery; Python-API-only operations | requirements per command |
[api-surface_stable-2026.2.md](_rewrite/api-surface_stable-2026.2.md) | the 32 operations, with source, headers, service handler, unit test count, and the 26 service operations never called | gap analysis of a service |
[unit-vectors_stable-2026.2.json](_rewrite/unit-vectors_stable-2026.2.json) | 213 tests with their 384 requests: method, path, query, headers, body, mocked status, normalised ({uuid}). report.command_classes_run_by_tests is empty | what the Python API sends for each case |
[wire-probe-404_stable-2026.2.json](_rewrite/wire-probe-404_stable-2026.2.json), [wire-probe-200_stable-2026.2.json](_rewrite/wire-probe-200_stable-2026.2.json) | 54 runs each: argv, exit, stdout, stderr, identity, discovery and all requests, with every route answering 404 (Barbican error shape) or {} | wire reference |
[wire-probe-shape_stable-2026.2.json](_rewrite/wire-probe-shape_stable-2026.2.json) | the same, with bodies written from the Barbican source | client output on plausible answers; not service behaviour |
[defect-repro_stable-2026.2.json](_rewrite/defect-repro_stable-2026.2.json) | the 86 cases cited in 02, with programmed routes, exit, stderr and requests | evidence; turn them into tests |
| Script | Role | Change from the shared tool |
|---|---|---|
[extract_plugin_stable-2026.2.py](_rewrite/extract_plugin_stable-2026.2.py) | imports the entry points, dumps parsers | filters calls to private attributes |
[probe_plugin_stable-2026.2.py](_rewrite/probe_plugin_stable-2026.2.py) | stub, and one real process per run | header-dependent version documents, per-resource URIs, option variants, shape mode, subprocess, discovery logged |
[capture_plugin_stable-2026.2.py](_rewrite/capture_plugin_stable-2026.2.py) | runs the unit tests with a recorder | records at the requests-mock adapter, since there is no check_parser; blocks outbound sockets; counts take_action calls |
[repro_defects_stable-2026.2.py](_rewrite/repro_defects_stable-2026.2.py) | programs the stub per case | new |
[build_contract_stable-2026.2.py](_rewrite/build_contract_stable-2026.2.py) | merges everything | hand-read static table; checks against the Barbican route table |
The generic rewrite_pack.py and conformance_capture.py were not used: they need an in-process service or functional tests, and functionaltests/ needs a live deployment (etc/functional_tests.conf:5).
Versions: python-barbicanclient 7.6.1.dev2 (setup-time version at this commit), python-openstackclient 10.3.1, osc-lib 4.7.0, cliff 4.16.0, keystoneauth1 5.17.0, requests-mock 1.12.1, Python 3.13.
To regenerate, from the report directory, with HOME set to an empty directory and OS_* unset:
python -I -B _rewrite/extract_plugin_stable-2026.2.py <repo> python-barbicanclient barbicanclient _rewrite
python -I -B _rewrite/probe_plugin_stable-2026.2.py _rewrite/cli-commands_stable-2026.2.json <dir-404> --mode 404 --repo <repo>
python -I -B _rewrite/probe_plugin_stable-2026.2.py _rewrite/cli-commands_stable-2026.2.json <dir-200> --mode 200 --repo <repo>
python -I -B _rewrite/probe_plugin_stable-2026.2.py _rewrite/cli-commands_stable-2026.2.json <dir-shape> --mode shape --repo <repo>
python -I -B _rewrite/capture_plugin_stable-2026.2.py <repo> _rewrite/cli-commands_stable-2026.2.json <run-1>/c.json <repo>/barbicanclient/tests <repo>
python -I -B _rewrite/capture_plugin_stable-2026.2.py <repo> _rewrite/cli-commands_stable-2026.2.json <run-2>/c.json <repo>/barbicanclient/tests <repo>
python -I -B _rewrite/repro_defects_stable-2026.2.py <repro.json> --repo <repo>
python -I -B _rewrite/build_contract_stable-2026.2.py . <dir-404>/wire-probe_stable-2026.2.json <dir-200>/wire-probe_stable-2026.2.json <dir-shape>/wire-probe_stable-2026.2.json <run-1>/c.json <run-2>/c.json <repro.json> <reverse-spec-barbican>/_rewrite/routes_stable-2026.2.json <generated-dir>
2. What the client requires from a key manager
These are hard requirements on any service that must work with this client unchanged. "Crash" means the client fails with a bare Python message [observed]. Every row is client behaviour; whether a real Barbican satisfies it was read, not run.
Discovery
| # | Requirement | If not | Source |
|---|---|---|---|
| 1 | service type key-manager in the catalog, with a public endpoint | Could not find requested endpoint or similar | barbicanclient/client.py:28 |
| 2 | GET <catalog URL> without token answers a version list; 300 is fine | fallback warning, then failures [observed] | keystoneauth1:keystoneauth1/discover.py:1500 |
| 3 | GET <catalog URL>/v1 with OpenStack-API-Version: key-manager 1.1 answers {"version": {"status": "CURRENT", "min_version": "1.0", "max_version": "1.2", "links": [...]}} | Invalid client version v1 if max_version is missing | barbicanclient/v1/client.py:147 |
| 4 | status STABLE, any case, means 1.0 only; do not send it from a 1.1+ answer | consumers refused locally | barbicanclient/v1/client.py:137 |
| 5 | with --endpoint, the same document at the endpoint as given, as a list {"versions": [...]} or a single version | barbicanclient/v1/client.py:114 | |
| 6 | the requested microversion is accepted in OpenStack-API-Version: key-manager 1.x, up to the announced maximum | 406 on the first real request | barbicanclient/v1/client.py:62 |
Requests to accept
| # | Requirement | Source |
|---|---|---|
| 7 | POST with a trailing slash on /secrets/, /orders/, /containers/, /secrets/{id}/consumers/, /containers/{id}/consumers/; GET, PUT and DELETE without it | barbicanclient/client.py:109 |
| 8 | DELETE /secrets/{id}/consumers and /containers/{id}/consumers with a JSON body | barbicanclient/v1/secrets.py:745, barbicanclient/v1/containers.py:774 |
| 9 | PUT /secrets/{id} with raw body and Content-Type: text/plain or application/octet-stream | barbicanclient/v1/secrets.py:536 |
| 10 | ?force=1 on DELETE /secrets/{id} as a boolean | barbicanclient/v1/secrets.py:601 |
| 11 | GET /secrets/{id}/payload honouring Accept, default text/plain on the CLI | barbicanclient/v1/secrets.py:319 |
| 12 | list parameters limit, offset, name, alg, mode, bits, secret_type, type, with alg and bits under these names | barbicanclient/v1/secrets.py:656 |
| 13 | expiration as ISO 8601 with offset: 2030-01-01T00:00:00+00:00 [observed] | barbicanclient/v1/secrets.py:344 |
| 14 | no-auth use: X-Project-Id | barbicanclient/client.py:51 |
Responses
| # | Requirement | If not | Source |
|---|---|---|---|
| 15 | **every JSON error has a top-level title**, optionally description. This includes 401s. A service in front of which keystonemiddleware answers has to live with 'title'; a rewrite should send title at the top level in its 401 body, in addition to error | crash D1 | barbicanclient/client.py:141 |
| 16 | the secret deletion refusal holds exactly Secret cannot be deleted as it has consumers. in title: description, with a 4xx | plain HTTPClientError | barbicanclient/v1/secrets.py:606 |
| 17 | secret documents hold only name, expiration, algorithm, bit_length, mode, payload, payload_content_type, payload_content_encoding, secret_ref, created, updated, content_types, status, secret_type, creator_id, consumers | crash D5 | barbicanclient/v1/secrets.py:129 |
| 18 | consumer entries hold only service, resource_type, resource_id, created, updated, status | crash D5 | barbicanclient/v1/secrets.py:70 |
| 19 | order documents hold type, meta, and only status, created, updated, order_ref, error_status_code, error_reason, sub_status, sub_status_message, creator_id, plus secret_ref on key or container_ref on asymmetric (not the other) | crash D5 | barbicanclient/v1/orders.py:229, barbicanclient/v1/orders.py:272 |
| 20 | meta of a key order holds only name, algorithm, mode, bit_length, expiration, payload_content_type; of asymmetric only those plus passphrase, pass_phrase | Invalid KeyOrder meta field, or crash | barbicanclient/v1/orders.py:226, barbicanclient/v1/orders.py:356 |
| 21 | order type is key or asymmetric, in any case | D6 | barbicanclient/v1/orders.py:344 |
| 22 | container documents hold type in generic, rsa, certificate, any case, and a secret_refs list of {name, secret_ref}; other fields are ignored | crash D21 | barbicanclient/v1/containers.py:549, barbicanclient/v1/containers.py:612 |
| 23 | POST answers a JSON object holding secret_ref, order_ref or container_ref. For containers, a non-empty body without the key is KeyError | D15, D22 | barbicanclient/v1/secrets.py:394, barbicanclient/v1/orders.py:210, barbicanclient/v1/containers.py:208 |
| 24 | PUT …/acl answers a JSON object; acl_ref is read but not required | D15 | barbicanclient/v1/acls.py:328 |
| 25 | GET …/acl answers operations as keys, each an object with optional project-access, users, created, updated. The service answers a default read for no ACL (barbican:barbican/api/controllers/acls.py:200); an empty object breaks acl user remove | ACL data for secret is not provided. | barbicanclient/v1/acls.py:356 |
| 26 | POST /secrets/{id}/consumers/ and POST /containers/{id}/consumers/ answer the parent document | crash | barbicanclient/v1/secrets.py:717, barbicanclient/v1/containers.py:752 |
| 27 | dates in created, updated, expiration parse with oslo's parse_isotime; a bad one is ValueError at construction | bare message | barbicanclient/v1/secrets.py:445 |
| 28 | text/plain payloads are valid UTF-8 | D10 | barbicanclient/v1/secrets.py:325 |
| 29 | secret_ref and similar end with the UUID | incorrectly specified | barbicanclient/base.py:45 |
| 30 | the name of a secret should be returned when set, or each display costs 10 requests. A service cannot fix this for unnamed secrets | D8 | barbicanclient/v1/secrets.py:468 |
Lists may omit the collection key without failing (barbicanclient/v1/secrets.py:680). total is read only by .total() (barbicanclient/base.py:75).
Decisions for a rewrite of the service
- Additive changes are not free. Requirements 17 to 20 mean a new field needs a new client, or a microversion that this client never asks for: it asks for at most 1.2.
- **
/cas**: not in Barbicanstable/2026.2, soca getandca listshould already fail; a rewrite need not add it. - **
POST /secrets/{id}/metadata/{key}** is refused with 405 by Barbican, while the client sends it (D24). Staying faithful keeps the bug; adding the route would be harmless, and makeadd_secret_metadatawork. This needs a decision. - Secret delete at 1.2: the 400 with the fixed text is a requirement, including for castellan through the same code.
3. Coverage of the service
From [api-surface_stable-2026.2.md](_rewrite/api-surface_stable-2026.2.md): 32 operations, of which 29 have a handler in the Barbican route table. The client never calls 26 service operations, among them transport keys, quotas, project quotas, secret stores, PATCH on ACLs, GET …/consumers/{id}, POST and DELETE on /containers/{id}/secrets, and container consumer listing. A rewrite that serves only this client could postpone those. castellan and the other callers were not surveyed for them.
4. For a rewrite of the CLI or library
| Topic | Rule | Source |
|---|---|---|
| Names | secret_order_create → secret order create | setup.cfg:37 |
| Python callers | castellan uses Client(version, session, endpoint), the managers, and the exception families. A CLI rewrite would not replace the library | castellan:castellan/key_manager/barbican_key_manager.py:183, castellan:castellan/key_manager/barbican_key_manager.py:432 |
| Plugin coupling | make_client reads instance.session, instance._region_name, instance.interface | barbicanclient/osc_plugin.py:28 |
| Do not copy | D1 to D26 in 02 § 4, unless compatibility with scripts demands it: D9 and D12 change visible output | |
| Legacy shell | deprecated, with no reason to re-implement it; functionaltests/cli/v1/behaviors/base_behaviors.py:30 still drives the CLI tests through it | barbicanclient/barbican.py:382 |
Recommendation [inferred]: keep the Python client, and make the service conform. The CLI is thin, but castellan needs the library, and its strictness on fields is what constrains the service. If the client can be patched, D5 (ignore unknown fields) and D1 (.get('title')) are the two fixes that buy the most.
5. Conformance: what was proved
| Evidence | Result | What it proves | What it does not prove |
|---|---|---|---|
| Parser dump | 23 of 23 classes imported, 0 failures | the catalogue is complete for the declared entry points | |
| Wire probes, 3 modes, 54 runs each | 23 of 23 commands sent a request in each mode; 52, 82 and 72 requests; base exit 0 for 0, 19 and 22 commands | real routes, headers, bodies, negotiation through the real shell | anything about a service: the stub is not one |
| Defect repros | 86 cases, 1 process each | how the client reads each answer | that a real service sends it |
| Unit tests with recorder | 254 of 254 pass in two runs; 213 tests hold requests, 384 requests, no outbound connection attempted | requests of the Python API | the CLI: 0 of 23 take_action ran (D26). The answers are test fixtures |
| Comparison of the two runs | 213 of 213 identical, with bodies and headers | determinism, after UUID normalisation | |
| Routes against the Barbican reverse-spec | 29 of 32 have a handler | the client's routes exist in that table | methods, bodies and statuses at run time |
"Replay is mandatory" is not met. Nothing here ran against Barbican. The barbican: cross-checks are readings of source code.
To close the gap: run probe_plugin_stable-2026.2.py with a real deployment in place of the stub, or run functionaltests/ against both the Python service and a rewrite. Those tests need a real deployment (etc/functional_tests.conf:5). Turn the shape mode into assertions only after that comparison.
6. Gaps found while building the pack
- 7 operations of the Python API are reached by no command. They ran only through a small Python driver, in the
pyapi-*cases. GET /secrets/{id}/consumershas 0 unit vectors: its tests mock_api.get(barbicanclient/tests/v1/test_consumers.py:133).- Not every option combination ran:
--os-key-manager-api-versionother than 1 and the listed variants did, TLS options did not. - The stub has no state, so a read after a write returns the fixed document.
acl-add-duplicateshows the body sent, not a stored ACL.
7. Non-functional requirements
- Latency: each process sends 2 identity and 2 discovery requests before its first business request [observed]. Discovery documents are on the hot path.
- Request volume: unnamed secrets multiply reads by 10 (D8). A service with many unnamed secrets sees
secret listas up to about 100 GET for one page of 10 [inferred from the 21 for 2]. - Secrets in transit: the payload travels in the POST body, base64 or plain; TLS comes from the deployment.
--debuglogs it (D14).
8. Checklist for a compatible service
- [ ]
key-managerin the catalog,publicinterface. - [ ] Version documents depend on the requested microversion:
stablefor 1.0 or no header,CURRENTwithmin_versionandmax_versionotherwise; 300 on the root. - [ ]
POSTroutes accept the trailing slash. - [ ]
DELETEon consumers reads a JSON body. - [ ] Every 4xx and 5xx JSON body has
title, anddescription; 401 included. - [ ] The refusal to delete holds the exact text;
force=1works at 1.2. - [ ] No field beyond the lists of § 2 in secrets, consumers, orders and
meta. - [ ]
POSTanswers JSON with the ref;PUT …/aclanswers JSON. - [ ]
GET …/aclgives a defaultreadwhen nothing is set. - [ ] Decision taken on
/casand onPOST /secrets/{id}/metadata/{key}. - [ ] The probe, pointed at the service, gives the same routes, and exit 0 with real resources.
Source: reverse-spec-python-barbicanclient/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.