Skip to content

Commit

Permalink
Support read charmcraft.yaml (canonical#977)
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 authored Jul 24, 2023
1 parent 2c82af3 commit 7da07cb
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 37 deletions.
119 changes: 82 additions & 37 deletions ops/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,57 +389,102 @@ def cleanup(self) -> None:
"""
self._backend._cleanup()

def _create_meta(self, charm_metadata: Optional[YAMLStringOrFile],
action_metadata: Optional[YAMLStringOrFile]) -> CharmMeta:
def _create_meta(self, charm_metadata_yaml: Optional[YAMLStringOrFile],
action_metadata_yaml: Optional[YAMLStringOrFile]) -> CharmMeta:
"""Create a CharmMeta object.
Handle the cases where a user doesn't supply explicit metadata snippets.
This will try to load metadata from ``<charm_dir>/charmcraft.yaml`` first, then
``<charm_dir>/metadata.yaml`` if charmcraft.yaml does not include metadata,
and ``<charm_dir>/actions.yaml`` if charmcraft.yaml does not include actions.
"""
filename = inspect.getfile(self._charm_cls)
charm_dir = pathlib.Path(filename).parents[1]

charm_metadata: Optional[Dict[str, Any]] = 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.is_file():
self._charm_dir = charm_dir
charmcraft_metadata = yaml.safe_load(charmcraft_meta.read_text())

# Load metadata from parameters if provided
if charm_metadata_yaml is not None:
if isinstance(charm_metadata_yaml, str):
charm_metadata_yaml = dedent(charm_metadata_yaml)
charm_metadata = yaml.safe_load(charm_metadata_yaml)
else:
# Check charmcraft.yaml for metadata if no metadata is provided
if charmcraft_metadata is not None:
meta_keys = ["name", "summary", "description"]
if any(key in charmcraft_metadata for key in meta_keys):
# Unrelated keys in the charmcraft.yaml file will be ignored.
charm_metadata = charmcraft_metadata

# Still no metadata, check metadata.yaml
if charm_metadata is None:
metadata_path = charm_dir / 'metadata.yaml'
if metadata_path.is_file():
charm_metadata = yaml.safe_load(metadata_path.read_text())
self._charm_dir = charm_dir

# Use default metadata if metadata is not found
if charm_metadata is None:
metadata_path = charm_dir / 'metadata.yaml'
if metadata_path.is_file():
charm_metadata = metadata_path.read_text()
self._charm_dir = charm_dir
else:
# The simplest of metadata that the framework can support
charm_metadata = 'name: test-charm'
elif isinstance(charm_metadata, str):
charm_metadata = dedent(charm_metadata)

if action_metadata is None:
actions_path = charm_dir / 'actions.yaml'
if actions_path.is_file():
action_metadata = actions_path.read_text()
self._charm_dir = charm_dir
elif isinstance(action_metadata, str):
action_metadata = dedent(action_metadata)

return CharmMeta.from_yaml(charm_metadata, action_metadata)

def _get_config(self, charm_config: Optional['YAMLStringOrFile']):
charm_metadata = {"name": "test-charm"}

action_metadata: Optional[Dict[str, Any]] = None
# Load actions from parameters if provided
if action_metadata_yaml is not None:
if isinstance(action_metadata_yaml, str):
action_metadata_yaml = dedent(action_metadata_yaml)
action_metadata = yaml.safe_load(action_metadata_yaml)
else:
# Check charmcraft.yaml for actions if no actions are provided
if charmcraft_metadata is not None and "actions" in charmcraft_metadata:
action_metadata = charmcraft_metadata["actions"]

# Still no actions, check actions.yaml
if action_metadata is None:
actions_path = charm_dir / 'actions.yaml'
if actions_path.is_file():
action_metadata = yaml.safe_load(actions_path.read_text())
self._charm_dir = charm_dir

return CharmMeta(charm_metadata, action_metadata)

def _get_config(self, charm_config_yaml: Optional['YAMLStringOrFile']):
"""If the user passed a config to Harness, use it.
Otherwise, attempt to load one from charm_dir/config.yaml.
Otherwise try to load config from ``<charm_dir>/charmcraft.yaml`` first, then
``<charm_dir>/config.yaml`` if charmcraft.yaml does not include config.
"""
filename = inspect.getfile(self._charm_cls)
charm_dir = pathlib.Path(filename).parents[1]
config: Optional[Dict[str, Any]] = None

if charm_config is None:
config_path = charm_dir / 'config.yaml'
if config_path.is_file():
charm_config = config_path.read_text()
self._charm_dir = charm_dir
else:
# The simplest of config that the framework can support
charm_config = '{}'
elif isinstance(charm_config, str):
charm_config = dedent(charm_config)

assert isinstance(charm_config, str) # type guard
config = yaml.safe_load(charm_config)
# Load config from parameters if provided
if charm_config_yaml is not None:
if isinstance(charm_config_yaml, str):
charm_config_yaml = dedent(charm_config_yaml)
config = yaml.safe_load(charm_config_yaml)
else:
# Check charmcraft.yaml for config if no config is provided
charmcraft_meta = charm_dir / "charmcraft.yaml"
if charmcraft_meta.is_file():
charmcraft_metadata: Dict[str, Any] = yaml.safe_load(charmcraft_meta.read_text())
config = charmcraft_metadata.get("config")

# Still no config, check config.yaml
if config is None:
config_path = charm_dir / 'config.yaml'
if config_path.is_file():
config = yaml.safe_load(config_path.read_text())
self._charm_dir = charm_dir

# Use default config if config is not found
if config is None:
config = {}

if not isinstance(config, dict):
raise TypeError(config)
Expand Down
77 changes: 77 additions & 0 deletions test/test_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,31 @@ def test_metadata_from_directory(self):
# The charm_dir also gets set
self.assertEqual(harness.framework.charm_dir, tmp)

def test_metadata_from_directory_charmcraft_yaml(self):
tmp = pathlib.Path(tempfile.mkdtemp())
self.addCleanup(shutil.rmtree, tmp)
charmcraft_filename = tmp / 'charmcraft.yaml'
charmcraft_filename.write_text(textwrap.dedent('''
type: charm
bases:
- build-on:
- name: ubuntu
channel: "22.04"
run-on:
- name: ubuntu
channel: "22.04"
name: my-charm
requires:
db:
interface: pgsql
'''))
harness = self._get_dummy_charm_harness(tmp)
harness.begin()
self.assertEqual(list(harness.model.relations), ['db'])
# The charm_dir also gets set
self.assertEqual(harness.framework.charm_dir, tmp)

def test_config_from_directory(self):
tmp = pathlib.Path(tempfile.mkdtemp())
self.addCleanup(shutil.rmtree, str(tmp))
Expand Down Expand Up @@ -1302,6 +1327,34 @@ def test_config_from_directory(self):
self.assertIsNone(harness._backend._config._defaults['opt_null'])
self.assertIsNone(harness._backend._config._defaults['opt_no_default'])

def test_config_from_directory_charmcraft_yaml(self):
tmp = pathlib.Path(tempfile.mkdtemp())
self.addCleanup(shutil.rmtree, tmp)
charmcraft_filename = tmp / 'charmcraft.yaml'
charmcraft_filename.write_text(textwrap.dedent('''
type: charm
bases:
- build-on:
- name: ubuntu
channel: "22.04"
run-on:
- name: ubuntu
channel: "22.04"
config:
options:
opt_str:
type: string
default: "val"
opt_int:
type: int
default: 1
'''))
harness = self._get_dummy_charm_harness(tmp)
self.assertEqual(harness.model.config['opt_str'], 'val')
self.assertEqual(harness.model.config['opt_int'], 1)
self.assertIsInstance(harness.model.config['opt_int'], int)

def test_set_model_name(self):
harness = ops.testing.Harness(ops.CharmBase, meta='''
name: test-charm
Expand Down Expand Up @@ -1721,6 +1774,30 @@ def test_actions_from_directory(self):
# The charm_dir also gets set
self.assertEqual(harness.framework.charm_dir, tmp)

def test_actions_from_directory_charmcraft_yaml(self):
tmp = pathlib.Path(tempfile.mkdtemp())
self.addCleanup(shutil.rmtree, tmp)
charmcraft_filename = tmp / 'charmcraft.yaml'
charmcraft_filename.write_text(textwrap.dedent('''
type: charm
bases:
- build-on:
- name: ubuntu
channel: "22.04"
run-on:
- name: ubuntu
channel: "22.04"
actions:
test:
description: a dummy action
'''))
harness = self._get_dummy_charm_harness(tmp)
harness.begin()
self.assertEqual(list(harness.framework.meta.actions), ['test'])
# The charm_dir also gets set
self.assertEqual(harness.framework.charm_dir, tmp)

def _get_dummy_charm_harness(self, tmp):
self._write_dummy_charm(tmp)
charm_mod = importlib.import_module('testcharm')
Expand Down

0 comments on commit 7da07cb

Please sign in to comment.