-
Notifications
You must be signed in to change notification settings - Fork 124
feat: add a low-level API for the Juju hook commands #2019
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6e8a6ec
7a7f64d
baeeb3b
e489165
9439cb8
d3b01a8
5e10803
b9471c5
c4fbe58
f805fb5
5d0fa86
b09a0a1
aff27a0
e39ad24
04aca1e
9a4526f
1254ea9
679da04
dd95cd2
0e19fbc
7abce36
cb56f94
0aea69e
51d41ff
c6861f3
da280f5
2bdc746
9279870
33e5af2
337452f
adf3807
fbfa117
aac9ce3
5b6a053
aba3052
2c1f6a2
97c299a
dfa4207
ec50c75
6ce9b0e
2124449
592a511
4e38491
bd9af43
69e3b10
0af9f33
3509476
328a75e
599e4bf
d03d102
a7b89be
fa98e99
46f47f7
6d5ec89
49294f3
5b60dc2
199e98b
fe49cb4
16cc50e
950ed25
74669d6
48a5076
a8c656b
03fa8c3
5058640
1b6640f
868de2d
62e6d47
e67490a
276b0a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,5 +10,5 @@ pebble | |
ops-testing | ||
ops-testing-harness | ||
ops-tracing | ||
ops-hookcmds | ||
``` | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
.. _ops_hookcmds: | ||
|
||
`ops.hookcmds` | ||
============== | ||
|
||
.. automodule:: ops.hookcmds |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
# Copyright 2025 Canonical Ltd. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
"""Low-level access to the Juju hook commands. | ||
|
||
Charm authors should use the :class:`ops.Model` (via ``self.model``) rather than | ||
directly running the hook commands, where possible. This module is primarily | ||
provided to help with developing charming alternatives to the Ops framework. | ||
|
||
Note: ``hookcmds`` is not covered by the semver policy that applies to the rest | ||
of Ops. We will do our best to avoid breaking changes, but we reserve the right | ||
to make breaking changes within this package if necessary, within the Ops 3.x | ||
series. | ||
|
||
All methods are 1:1 mapping to Juju hook commands. This is a *low-level* API, | ||
available for charm use, but expected to be used via higher-level wrappers. | ||
|
||
See `Juju | Hook commands <https://documentation.ubuntu.com/juju/3.6/reference/hook-command/>`_ | ||
and `Juju | Hook command list <https://documentation.ubuntu.com/juju/3.6/reference/hook-command/list-of-hook-commands/>`_ | ||
for a list of all Juju hook commands. | ||
""" | ||
|
||
from __future__ import annotations | ||
|
||
from ._action import action_fail, action_get, action_log, action_set | ||
from ._other import ( | ||
app_version_set, | ||
config_get, | ||
credential_get, | ||
goal_state, | ||
is_leader, | ||
juju_log, | ||
juju_reboot, | ||
network_get, | ||
resource_get, | ||
) | ||
from ._port import close_port, open_port, opened_ports | ||
from ._relation import relation_get, relation_ids, relation_list, relation_model_get, relation_set | ||
from ._secret import ( | ||
secret_add, | ||
secret_get, | ||
secret_grant, | ||
secret_ids, | ||
secret_info_get, | ||
secret_remove, | ||
secret_revoke, | ||
secret_set, | ||
) | ||
from ._state import state_delete, state_get, state_set | ||
from ._status import status_get, status_set | ||
from ._storage import storage_add, storage_get, storage_list | ||
from ._types import ( | ||
Address, | ||
AppStatus, | ||
BindAddress, | ||
CloudCredential, | ||
CloudSpec, | ||
Goal, | ||
GoalState, | ||
Network, | ||
Port, | ||
RelationModel, | ||
SecretInfo, | ||
SecretRotate, | ||
SettableStatusName, | ||
StatusName, | ||
Storage, | ||
UnitStatus, | ||
) | ||
from ._utils import Error | ||
|
||
__all__ = [ | ||
'Address', | ||
'AppStatus', | ||
'BindAddress', | ||
'CloudCredential', | ||
'CloudSpec', | ||
'Error', | ||
'Goal', | ||
'GoalState', | ||
'Network', | ||
'Port', | ||
'RelationModel', | ||
'SecretInfo', | ||
'SecretRotate', | ||
'SettableStatusName', | ||
'StatusName', | ||
'Storage', | ||
'UnitStatus', | ||
'action_fail', | ||
'action_get', | ||
'action_log', | ||
'action_set', | ||
'app_version_set', | ||
'close_port', | ||
'config_get', | ||
'credential_get', | ||
'goal_state', | ||
'is_leader', | ||
'juju_log', | ||
'juju_reboot', | ||
'network_get', | ||
'open_port', | ||
'opened_ports', | ||
'relation_get', | ||
'relation_ids', | ||
'relation_list', | ||
'relation_model_get', | ||
'relation_set', | ||
'resource_get', | ||
'secret_add', | ||
'secret_get', | ||
'secret_grant', | ||
'secret_ids', | ||
'secret_info_get', | ||
'secret_remove', | ||
'secret_revoke', | ||
'secret_set', | ||
'state_delete', | ||
'state_get', | ||
'state_set', | ||
'status_get', | ||
'status_set', | ||
'storage_add', | ||
'storage_get', | ||
'storage_list', | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
# Copyright 2025 Canonical Ltd. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
from __future__ import annotations | ||
|
||
import json | ||
from typing import ( | ||
Any, | ||
Mapping, | ||
MutableMapping, | ||
cast, | ||
overload, | ||
) | ||
|
||
from ._utils import run | ||
|
||
|
||
def format_result_dict( | ||
input: Mapping[str, Any], | ||
parent_key: str | None = None, | ||
output: dict[str, str] | None = None, | ||
) -> dict[str, str]: | ||
"""Turn a nested dictionary into a flattened dictionary, using '.' as a key separator. | ||
This is used to allow nested dictionaries to be translated into the dotted | ||
format required by the Juju `action-set` hook command in order to set nested | ||
data on an action. | ||
Example:: | ||
>>> test_dict = {'a': {'b': 1, 'c': 2}} | ||
>>> format_result_dict(test_dict) | ||
{'a.b': 1, 'a.c': 2} | ||
Arguments: | ||
input: The dictionary to flatten | ||
parent_key: The string to prepend to dictionary's keys | ||
output: The current dictionary to be returned, which may or may not yet | ||
be completely flat | ||
Returns: | ||
A flattened dictionary | ||
Raises: | ||
ValueError: if the dict is passed with a mix of dotted/non-dotted keys | ||
that expand out to result in duplicate keys. For example: | ||
``{'a': {'b': 1}, 'a.b': 2}``. | ||
""" | ||
Comment on lines
+29
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not an exported function, so I wonder if a large, detailed doc string is called for. Minor: it would be nice to document what happens to other types of values, e.g.:
That being said, I'm not sure where to document this. |
||
output_: dict[str, str] = output or {} | ||
|
||
for key, value in input.items(): | ||
if parent_key: | ||
key = f'{parent_key}.{key}' | ||
|
||
if isinstance(value, MutableMapping): | ||
value = cast('dict[str, Any]', value) | ||
output_ = format_result_dict(value, key, output_) | ||
elif key in output_: | ||
raise ValueError( | ||
f"duplicate key detected in dictionary passed to 'action-set': {key!r}" | ||
) | ||
else: | ||
output_[key] = value | ||
|
||
return output_ | ||
|
||
|
||
def action_fail(message: str | None = None): | ||
"""Set action fail status with message. | ||
For more details, see: | ||
`Juju | Hook commands | action-fail <https://documentation.ubuntu.com/juju/3.6/reference/hook-command/list-of-hook-commands/action-fail/>`_ | ||
Args: | ||
message: the failure error message. Juju will provide a default message | ||
if one is not provided. | ||
""" | ||
args: list[str] = [] | ||
if message is not None: | ||
# The '--' allows messages that start with a hyphen. | ||
args.extend(['--', message]) | ||
run('action-fail', *args) | ||
|
||
|
||
@overload | ||
def action_get() -> dict[str, Any]: ... | ||
@overload | ||
def action_get(key: str) -> str: ... | ||
def action_get(key: str | None = None) -> dict[str, Any] | str: | ||
"""Get action parameters. | ||
``action_get`` returns the value of the parameter at the given key. If a | ||
dotted key (for example foo.bar) is passed, ``action_get`` will recurse into | ||
the parameter map as needed. | ||
For more details, see: | ||
`Juju | Hook commands | action-get <https://documentation.ubuntu.com/juju/3.6/reference/hook-command/list-of-hook-commands/action-get/>`_ | ||
Args: | ||
key: The key of the action parameter to retrieve. If not provided, all | ||
parameters will be returned. | ||
""" | ||
dimaqq marked this conversation as resolved.
Show resolved
Hide resolved
|
||
args = ['--format=json'] | ||
if key is not None: | ||
args.append(key) | ||
stdout = run('action-get', *args) | ||
result = ( | ||
cast('dict[str, Any]', json.loads(stdout)) | ||
if key is None | ||
else cast('str', json.loads(stdout)) | ||
) | ||
return result | ||
|
||
|
||
def action_log(message: str): | ||
"""Record a progress message for the current action. | ||
For more details, see: | ||
`Juju | Hook commands | action-log <https://documentation.ubuntu.com/juju/3.6/reference/hook-command/list-of-hook-commands/action-log/>`_ | ||
Args: | ||
message: The progress message to provide to the Juju user. | ||
""" | ||
# The '--' allows messages that start with a hyphen. | ||
run('action-log', '--', message) | ||
|
||
|
||
def action_set(results: Mapping[str, Any]): | ||
"""Set action results. | ||
For more details, see: | ||
`Juju | Hook commands | action-set <https://documentation.ubuntu.com/juju/3.6/reference/hook-command/list-of-hook-commands/action-set/>`_ | ||
Args: | ||
results: The results map of the action, provided to the Juju user. | ||
""" | ||
# The Juju action-set hook tool cannot interpret nested dicts, so we use a | ||
# helper to flatten out any nested dict structures into a dotted notation. | ||
flat_results = format_result_dict(results) | ||
run('action-set', *[f'{k}={v}' for k, v in flat_results.items()]) |
Uh oh!
There was an error while loading. Please reload this page.