api: round out Agent, Operator, Catalog, Health, Coordinate, Event endpoints#123
Merged
Merged
Conversation
added 9 commits
July 3, 2026 10:50
get()/put()/post() all accept a request body, but delete() didn't,
even though some Consul DELETE endpoints require one (e.g. DELETE
/v1/operator/keyring needs {"Key": ...} -- a bodyless request gets
rejected with "Request decode failed: EOF"). Widens delete() to
match, defaulting to "" so every existing caller is unaffected.
Adds Agent.leave(), Agent.reload(), Agent.metrics() (JSON and Prometheus formats), Agent.monitor() (single-shot, not true streaming -- documented limitation, see docstring), and a nested Agent.Token class covering PUT /v1/agent/token/:type for all five token types (default/agent/agent_recovery/replication/ config_file_service_registration).
Adds raft_remove_peer(), raft_transfer_leader(), autopilot configuration read/write (with cas support), autopilot_health() (handles the real 429-with-body "unhealthy" response instead of raising), keyring list/install/use/remove, and usage(). CE-only, per the earlier decision to exclude Enterprise entirely (License, Area, Network Segments, and autopilot state's Enterprise fields are intentionally not implemented). Found and fixed the same CB.boolean()-hides-the-real-answer bug as config.py's cas support: PUT .../autopilot/configuration with cas always returns HTTP 200, encoding actual success/failure as a JSON boolean body -- using CB.json() there instead, matching the Config.delete() precedent.
Adds filter (bexpr) to nodes/services/service/connect/node, peer and merge-central-config to service/connect, a new gateway_services() method, and SkipNodeUpdate/TaggedAddresses support on register(). Uncommented and extended tests/api/test_catalog.py, which previously had no active tests.
Adds filter (bexpr) to node/checks/service/connect/state, peer and merge-central-config to service/connect, and a new ingress() method for GET /v1/health/ingress/:service (deliberately without peer, matching Consul's documented ingress limitations rather than its more lenient runtime behavior).
Adds Coordinate.node() (GET /v1/coordinate/node/:node) and Coordinate.update() (PUT /v1/coordinate/update). Note: node() 404s in a fresh single-node dev-mode Consul until a coordinate has actually been written for that node -- LAN coordinates aren't pre-populated by gossip the way WAN datacenter coordinates are.
Adds dc to fire() and node/service/tag filters to list(). Verified live that node/service/tag actually only take effect at fire time (gossip propagation to matching agents), not as a list-time filter on already-stored events -- tests assert accordingly rather than asserting unverified filtering behavior.
Marks Agent, Operator, Catalog, Health, Coordinate, and Event as fully covered per the endpoints added in the preceding commits.
CI caught what my local mypy/ruff-only verification missed: adding filter/peer/merge-central-config pushed _service to 13 branches, over pylint's default limit of 12. Extracted the tag-normalization logic into a small typed _tag_params() static helper, which also required a full type signature to satisfy disallow_untyped_calls.
mougams
approved these changes
Jul 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 3 of the ongoing API-parity work (follow-up to #120/#121/#122) — closes out the remaining gaps tracked in
ENDPOINT_STATUS.mdfor six existing endpoint groups, each as its own commit:leave(),reload(),metrics()(JSON + Prometheus),monitor()(single-shot, not true streaming — documented limitation), and a nestedAgent.Tokenclass for all fivePUT /v1/agent/token/:typevariants.filter/peer/merge-central-configparams, newgateway_services(),SkipNodeUpdate/TaggedAddressesonregister().filter/peer/merge-central-configparams, newingress().node()read,update()write.dconfire(),node/service/tagonlist().HTTPClient.delete()gained adataparam (needed byDELETE /v1/operator/keyring, which requires a JSON body — every other write method already had this, delete() was the one gap).Bugs this surfaced (same pattern found twice)
PUT /v1/operator/autopilot/configurationwithcasbehaves exactly likeConfig.set()'s cas support from #121: always returns HTTP 200, with actual success/failure encoded as a JSON boolean body. Using status-onlyCB.boolean()there would have silently reported cas failures as successes — caught by a real integration test, fixed to useCB.json()matching the establishedConfigprecedent.Other real findings from live-probing (not just docs)
GET /v1/coordinate/node/:node404s in a fresh dev-mode Consul until a coordinate is actually written for that node — LAN coordinates aren't pre-populated by gossip the waycoordinate/datacenters(WAN) is.Event.list()'snode/service/tagparams are accepted without error but have no observable filtering effect at list-time in a single-node topology — they appear to only gate gossip propagation at fire-time. Tests assert the real behavior, not the docs' implication./catalog/services' bexpr tag selector isServiceTags, notTags./catalog/node/:node's filter evaluates against each entry of theServicesmap, not the top-level document.Health.ingress()deliberately excludespeer/merge-central-configeven though Consul doesn't error if you pass them — the canonical docs explicitly say ingress doesn't support them, so the client matches the documented contract over the lenient runtime behavior.Test plan
mypy consul/ --no-sqlite-cache— clean (32 source files)ruff check ./ruff format --diff .— cleantests/test_features.py)Agent.monitor()'s test is intentionally scoped to the fast-fail path only (invalid loglevel → 400) — the success path is a genuinely unbounded blocking call with no per-request timeout anywhere in this library's transport layer, and there is no safe way to bound it from a test without either leaking a thread (Python'sconcurrent.futuresatexit handler joins worker threads unconditionally, which would hang interpreter shutdown) or adding timeout support tostd.py/aio.py, which is out of scope here.