Skip to content

fix!: ops.hookcmds returns all the data that the hook commands output - #2805

Open
tonyandrewmeyer wants to merge 6 commits into
canonical:mainfrom
tonyandrewmeyer:hookcmds-missing-fields
Open

tonyandrewmeyer wants to merge 6 commits into
canonical:mainfrom
tonyandrewmeyer:hookcmds-missing-fields

Conversation

@tonyandrewmeyer

@tonyandrewmeyer tonyandrewmeyer commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

This fixes the places where ops.hookcmds drops or mis-parses data that Juju's hook commands output. I found them by comparing every hook command in the Juju 2.9, 3.6, and main (4.x) source with what we parse, and then running each command against real controllers (details below).

  • status_get(app=True) always returned units={}. Juju nests the unit statuses inside application-status rather than at the top level - our test (and the AppStatusDict type) had the wrong shape, which is why it passed.
  • secret_info_get ignored owner (Juju 3.1+) and access (Juju 3.4+, the list of grants, omitted when there are none). These are now SecretInfo.owner and SecretInfo.access, with a new SecretAccess class for the grants.
  • opened_ports(endpoints=True) mis-parsed ports with more than one endpoint, because Juju separates the endpoints with ", ": 80/tcp (db, web) came back as Port(protocol='tcp (db,', port=80, endpoints=['web']). The test used (ep1,ep2), which Juju never produces.
  • SecretInfo.description was None rather than '' when Juju omits it (and label would have been too, although Juju always sends that). ops.model.SecretInfo still gets None for a missing description, as before.
  • Juju's secret-set accepts --owner but ignores it (in every 3.x release and in 4), so the owner argument to secret_set could never do anything. It's now deprecated, and we no longer pass --owner. The flag is only there because secret-set shares its flags with secret-add (since unit-owned secrets arrived in Juju 3.0): secret-set checks the value and then drops it, so this never worked, rather than being a deprecation we missed.
  • On Juju 2.9, relation_model_get and the secret_* functions raised a bare FileNotFoundError rather than hookcmds.Error, because the commands don't exist. A missing hook command now raises hookcmds.Error, with return code 127 (the shell's "command not found" code) and the FileNotFoundError as the cause. That's a breaking change for anyone catching FileNotFoundError from ops.hookcmds (which I believe is acceptable), but ops.model converts it back, so ops.Model behaves as before (I would change this in 4.0).
  • Smaller typing fixes: juju_log accepts CRITICAL (ops.model already passed it, with a type: ignore), config_get(key) and relation_get(key=...) can return None (Juju outputs null for a missing key), secret_revoke's relation_id has a default, and relation_list's internal annotations were swapped.

There's one small ops.model change: the pre-3.6 metadata fix in secret_set no longer passes an empty --label. Juju ignores an empty label, so this isn't a behaviour change, but it keeps the existing tests valid now that hookcmds gives '' rather than None.

Other than that, ops.Model behaves as before, so I haven't done a Hyrum run.

How I tested things

For the live testing, I used a Multipass VM per Juju version (2.9.60, 3.6.29, and 4.0.15, all on LXD), and deployed a minimal machine charm: a bash dispatch, a peer relation, a requires/provides pair related to a second application, an extra binding, filesystem storage, a file resource, config options with and without defaults, and an action. Each application had two units. I then used juju exec (juju run on 2.9) to run each hook command raw with --format=json alongside the matching ops.hookcmds call, on both the leader and a non-leader. That covered config-get, credential-get, goal-state, is-leader, network-get (including -r), opened-ports (with and without --endpoints), relation-ids, relation-list, relation-get, relation-model-get, resource-get, state-get, status-get (unit and application), storage-list, storage-get, and the secret commands (application-owned and unit-owned secrets, a relation grant and a single-unit grant, secret-info-get by ID and by label, and secret-set --owner). On 3.6, I also checked action-get and storage-get without -s from within the action and storage-attached hooks.

Without this change, the status, opened-ports, and secret problems reproduced on all three versions (other than the secrets ones on 2.9, which doesn't have secrets). With this branch, the same probe gives the expected values everywhere: the units are in AppStatus.units, the endpoints are ['db', 'web'], SecretInfo has the owner and grants (for example SecretAccess(target='unit-b-0', scope='relation-a.db#b.web', role='view')) on 3.6 and 4.0, and relation_get(key=...) returns None for a missing key. On 3.6 and 4.0, secret_set(owner=...) warns and the secret's owner stays unit, and on 4.0 a CRITICAL juju_log shows up in juju debug-log at that level. Everything else already matched Juju's output.

I've also extended the ops.hookcmds integration tests to cover each of these fixes, other than the Juju 2.9 one, as CI doesn't test against 2.9. They pass on my fork on machine and Kubernetes, with Juju 3.6, 4.0 (stable and edge), and 4.1 (edge).

Follow-up work:

  • ops.model.SecretInfo doesn't have owner or access either. Adding those seems like a separate feature PR, since it also needs ops.testing support.

tonyandrewmeyer and others added 3 commits October 8, 2026 13:38
Compare every hook command's output with Juju 2.9, 3.6, and 4 (from the Juju
source and live runs), and fix the places where ops.hookcmds dropped or
mis-parsed data:

- status_get(app=True) always returned no units, because Juju nests the unit
  statuses inside "application-status" rather than at the top level.
- secret_info_get ignored the secret's owner and the access grants. Add
  SecretInfo.owner and SecretInfo.access, with a new SecretAccess type.
- opened_ports(endpoints=True) mis-parsed ports with more than one endpoint,
  because Juju separates the endpoints with ", ".
- SecretInfo.description and label could be None despite being typed str.

Also:

- Deprecate secret_set's owner argument and stop passing --owner, as Juju
  ignores it when updating a secret.
- Add config_get(all=True) and state_get(strict=True) for the matching Juju
  flags.
- Allow the CRITICAL level in juju_log, include None in the return types
  of config_get(key) and relation_get(key=...), give secret_revoke's
  relation_id a default, and correct relation_list's internal annotations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove config_get's all and state_get's strict arguments again: both were
deliberately left out, as there's no use-case for them. Add a comment for
state_get matching the existing one for config_get, and a comment listing
the hook commands that deliberately have no wrapper.

When a hook command doesn't exist (for example, the secret commands and
relation-model-get on Juju 2.9), ops.hookcmds now raises hookcmds.Error
with return code 127 and the FileNotFoundError as the cause, rather than
letting the FileNotFoundError escape. This is a breaking change for code
that catches FileNotFoundError from ops.hookcmds directly. The model
backend converts the error back to the original FileNotFoundError, so
ops.Model behaviour is unchanged.

Extend the hookcmds integration tests to cover the fixes: the app status
units, the secret owner and access grants, the deprecated secret_set owner,
unset secret labels and descriptions, ports with more than one endpoint,
missing config and relation keys, and logging at the CRITICAL level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use the deprecated directive for secret_set's owner argument, as the rest
of ops does, explain why the model backend maps an empty secret
description to None, and use the active voice in relation_get's
docstring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tonyandrewmeyer
tonyandrewmeyer marked this pull request as ready for review October 8, 2026 04:44
hookcmds gives '' for a missing label, the same as it does for a missing
description, but ops.model.SecretInfo has always used None (and both
testing backends still do).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@tonyandrewmeyer tonyandrewmeyer changed the title fix: ops.hookcmds returns all the data that the hook commands output fix!: ops.hookcmds returns all the data that the hook commands output Oct 15, 2026
tonyandrewmeyer and others added 2 commits October 15, 2026 13:17
…ng hook command

The command was never run, so the generic message was misleading in
juju debug-log.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Matches the SecretRotate precedent in the same module. Juju 3.6 has
view, rotate and manage; Juju 4 drops rotate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant