Skip to content

Commit

Permalink
Support read charmcraft.yaml
Browse files Browse the repository at this point in the history
Try read unified charmcraft.yaml first, that may included metadata,
actions, and config. Ignore metadata.yaml, actions.yaml, config.yaml if
they are exists in charmcraft.yaml
  • Loading branch information
syu-w committed Jul 18, 2023
1 parent dd4865f commit 3cb70d8
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 14 deletions.
10 changes: 7 additions & 3 deletions ops/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1118,18 +1118,22 @@ def __init__(self, raw: Optional[Dict[str, Any]] = None,
@classmethod
def from_yaml(
cls, metadata: Union[str, TextIO],
actions: Optional[Union[str, TextIO]] = None) -> 'CharmMeta':
actions: Optional[Union[str, TextIO, Dict[str, Any]]] = None) -> 'CharmMeta':
"""Instantiate a :class:`CharmMeta` from a YAML description of ``metadata.yaml``.
Args:
metadata: A YAML description of charm metadata (name, relations, etc.)
This can be a simple string, or a file-like object (passed to ``yaml.safe_load``).
actions: YAML description of Actions for this charm (e.g., actions.yaml)
actions: YAML description of Actions for this charm (e.g., actions.yaml),
or a mapped dictionary of actions that could come from charmcraft.yaml.
"""
meta = yaml.safe_load(metadata)
raw_actions = {}
if actions is not None:
raw_actions = cast(Optional[Dict[str, Any]], yaml.safe_load(actions))
if isinstance(actions, dict):
raw_actions = actions
else:
raw_actions = cast(Optional[Dict[str, Any]], yaml.safe_load(actions))
if raw_actions is None:
raw_actions = {}
return cls(meta, raw_actions)
Expand Down
31 changes: 25 additions & 6 deletions ops/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import ops.framework
import ops.model
import ops.storage
from ops._private import yaml
from ops.charm import CharmMeta
from ops.jujuversion import JujuVersion
from ops.log import setup_root_logging
Expand Down Expand Up @@ -374,6 +375,8 @@ def main(charm_class: Type[ops.charm.CharmBase],
are running on a new enough Juju default to controller-side storage,
otherwise local storage is used.
"""
metadata: Optional[str] = None
actions_metadata: Optional[Union[str, Dict[str, Any]]] = None
charm_dir = _get_charm_dir()

model_backend = ops.model._ModelBackend()
Expand All @@ -384,12 +387,28 @@ def main(charm_class: Type[ops.charm.CharmBase],
dispatcher = _Dispatcher(charm_dir)
dispatcher.run_any_legacy_hook()

metadata = (charm_dir / 'metadata.yaml').read_text()
actions_meta = charm_dir / 'actions.yaml'
if actions_meta.exists():
actions_metadata = actions_meta.read_text()
else:
actions_metadata = None
# Check charmcraft.yaml first and see if it has the metadata data in it.
charmcraft_meta = charm_dir / "charmcraft.yaml"
if charmcraft_meta.exists():
charmcraft_yaml = charmcraft_meta.read_text()
charmcraft_metadata: dict[str, Any] = yaml.safe_load(charmcraft_yaml)
if any(
meta_key in charmcraft_metadata for meta_key in ("name", "summary", "description")
):
# Unrelated keys in the charmcraft.yaml file will be ignored.
metadata = charmcraft_yaml

# This could be None if the charmcraft.yaml not include the actions key.
# Should fall back to the actions.yaml file.
actions_metadata = charmcraft_metadata.get("actions")

if not metadata:
metadata = (charm_dir / "metadata.yaml").read_text()

if not actions_metadata:
actions_meta = charm_dir / "actions.yaml"
if actions_meta.exists():
actions_metadata = actions_meta.read_text()

meta = CharmMeta.from_yaml(metadata, actions_metadata)
model = ops.model.Model(meta, model_backend)
Expand Down
49 changes: 44 additions & 5 deletions ops/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,9 +394,30 @@ def _create_meta(self, charm_metadata: Optional[YAMLStringOrFile],
"""Create a CharmMeta object.
Handle the cases where a user doesn't supply explicit metadata snippets.
Will try to load metadata/config from charm_dir/charmcraft.yaml first, then
charm_dir/metadata.yaml and/or charm_dir/actions.yaml if charmcraft.yaml
not include metadata and/or actions.
"""
filename = inspect.getfile(self._charm_cls)
charm_dir = pathlib.Path(filename).parents[1]
charmcraft_yaml: Optional[str] = None
charmcraft_metadata: Optional[dict[str, Any]] = None

# Check charmcraft.yaml and load it if it exists
charmcraft_meta = charm_dir / "charmcraft.yaml"
if charmcraft_meta.exists():
self._charm_dir = charm_dir
charmcraft_yaml = charmcraft_meta.read_text()
charmcraft_metadata = yaml.safe_load(charmcraft_yaml)

# Add metadata from charmcraft.yaml if no metadata is provided
# Should fall back to metadata.yaml if both are not provided
if charm_metadata is None and charmcraft_metadata:
if any(
meta_key in charmcraft_metadata for meta_key in ("name", "summary", "description")
):
# Unrelated keys in the charmcraft.yaml file will be ignored.
charm_metadata = charmcraft_yaml

if charm_metadata is None:
metadata_path = charm_dir / 'metadata.yaml'
Expand All @@ -409,6 +430,11 @@ def _create_meta(self, charm_metadata: Optional[YAMLStringOrFile],
elif isinstance(charm_metadata, str):
charm_metadata = dedent(charm_metadata)

# Add actions from charmcraft.yaml if no actions are provided
# Should fall back to actions.yaml if both are not provided
if action_metadata is None and charmcraft_metadata:
action_metadata = charmcraft_metadata.get("actions")

if action_metadata is None:
actions_path = charm_dir / 'actions.yaml'
if actions_path.is_file():
Expand All @@ -422,12 +448,23 @@ def _create_meta(self, charm_metadata: Optional[YAMLStringOrFile],
def _get_config(self, charm_config: Optional['YAMLStringOrFile']):
"""If the user passed a config to Harness, use it.
Otherwise, attempt to load one from charm_dir/config.yaml.
Otherwise, attempt to load one from charm_dir/charmcraft.yaml, then
charm_dir/config.yaml if config is not provided in charmcraft.yaml.
"""
filename = inspect.getfile(self._charm_cls)
charm_dir = pathlib.Path(filename).parents[1]

if charm_config is None:
config: Optional[dict[str, Any]] = None

# Check charmcraft.yaml first if no config is provided
charmcraft_meta = charm_dir / "charmcraft.yaml"
if charm_config is None and charmcraft_meta.exists():
self._charm_dir = charm_dir
charmcraft_yaml = charmcraft_meta.read_text()
charmcraft_metadata: dict[str, Any] = yaml.safe_load(charmcraft_yaml)
config = charmcraft_metadata.get("config")

# Add config from charmcraft.yaml if no config is provided
if config is None and charm_config is None:
config_path = charm_dir / 'config.yaml'
if config_path.is_file():
charm_config = config_path.read_text()
Expand All @@ -438,8 +475,10 @@ def _get_config(self, charm_config: Optional['YAMLStringOrFile']):
elif isinstance(charm_config, str):
charm_config = dedent(charm_config)

assert isinstance(charm_config, str) # type guard
config = yaml.safe_load(charm_config)
# If we have a config dict already, ignore the charm_config
if not config:
assert isinstance(charm_config, str) # type guard
config = yaml.safe_load(charm_config)

if not isinstance(config, dict):
raise TypeError(config)
Expand Down

0 comments on commit 3cb70d8

Please sign in to comment.