mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-07-17 22:14:25 +02:00
Implemented that schemas are recognized
This commit is contained in:
parent
3fed9eb75a
commit
b6eb73dee4
@ -53,7 +53,8 @@ class DefaultsGenerator:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if not config_file.exists():
|
if not config_file.exists():
|
||||||
self.log(f"Skipping {role_name}: config/main.yml missing")
|
self.log(f"Config missing for {role_name}, adding empty defaults for '{application_id}'")
|
||||||
|
result["defaults_applications"][application_id] = {}
|
||||||
continue
|
continue
|
||||||
|
|
||||||
config_data = load_yaml_file(config_file)
|
config_data = load_yaml_file(config_file)
|
||||||
|
@ -18,20 +18,6 @@ vendor_and_product_id: ""
|
|||||||
|
|
||||||
The `vendor_and_product_id` variable is required and should be set to the vendor and product ID of the MSI laptop.
|
The `vendor_and_product_id` variable is required and should be set to the vendor and product ID of the MSI laptop.
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- `sys-pgm-aur`
|
|
||||||
|
|
||||||
## Example Playbook
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- hosts: all
|
|
||||||
roles:
|
|
||||||
- keyboard-color
|
|
||||||
vars:
|
|
||||||
vendor_and_product_id: "your_vendor_and_product_id"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Author
|
## Author
|
||||||
|
|
||||||
This role was created by [Kevin Veen-Birkenbach](https://github.com/kevinveenbirkenbach).
|
This role was created by [Kevin Veen-Birkenbach](https://github.com/kevinveenbirkenbach).
|
||||||
|
2
roles/drv-msi-keyboard-color/Todo.md
Normal file
2
roles/drv-msi-keyboard-color/Todo.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Todo
|
||||||
|
- Implement schema
|
1
roles/drv-msi-keyboard-color/schema/main.yml
Normal file
1
roles/drv-msi-keyboard-color/schema/main.yml
Normal file
@ -0,0 +1 @@
|
|||||||
|
vendor_and_product_id: "" # @todo schema needs to be implemented
|
@ -1 +1,2 @@
|
|||||||
application_id: drv-msi-keyboard-color
|
application_id: drv-msi-keyboard-color
|
||||||
|
vendor_and_product_id: "{{ applications | get_app_conf(application_id, 'vendor_and_product_id') }}"
|
||||||
|
@ -7,7 +7,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import yaml # requires PyYAML
|
import yaml # requires PyYAML
|
||||||
from filter_plugins.get_role import get_role
|
from filter_plugins.get_role import get_role
|
||||||
|
from filter_plugins.get_app_conf import get_app_conf, ConfigEntryNotSetError
|
||||||
|
|
||||||
class TestGetAppConfPaths(unittest.TestCase):
|
class TestGetAppConfPaths(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -88,12 +88,19 @@ class TestGetAppConfPaths(unittest.TestCase):
|
|||||||
cur = cur[k]
|
cur = cur[k]
|
||||||
|
|
||||||
def test_literal_paths(self):
|
def test_literal_paths(self):
|
||||||
# Check each literal path exists
|
# Check each literal path exists or is allowed by schema
|
||||||
for app_id, paths in self.literal_paths.items():
|
for app_id, paths in self.literal_paths.items():
|
||||||
with self.subTest(app=app_id):
|
with self.subTest(app=app_id):
|
||||||
self.assertIn(app_id, self.defaults_app, f"App '{app_id}' missing in defaults_applications")
|
self.assertIn(app_id, self.defaults_app, f"App '{app_id}' missing in defaults_applications")
|
||||||
for dotted, occs in paths.items():
|
for dotted, occs in paths.items():
|
||||||
with self.subTest(path=dotted):
|
with self.subTest(path=dotted):
|
||||||
|
try:
|
||||||
|
# will raise ConfigEntryNotSetError if defined in schema but not set
|
||||||
|
get_app_conf(self.defaults_app, app_id, dotted, strict=True)
|
||||||
|
except ConfigEntryNotSetError:
|
||||||
|
# defined in schema but not set: acceptable
|
||||||
|
continue
|
||||||
|
# otherwise, perform static validation
|
||||||
self._validate(app_id, dotted, occs)
|
self._validate(app_id, dotted, occs)
|
||||||
|
|
||||||
def test_variable_paths(self):
|
def test_variable_paths(self):
|
||||||
@ -103,6 +110,13 @@ class TestGetAppConfPaths(unittest.TestCase):
|
|||||||
for dotted, occs in self.variable_paths.items():
|
for dotted, occs in self.variable_paths.items():
|
||||||
with self.subTest(path=dotted):
|
with self.subTest(path=dotted):
|
||||||
found = False
|
found = False
|
||||||
|
# schema-defined entries: acceptable if defined in any role schema
|
||||||
|
for schema in self.role_schemas.values():
|
||||||
|
if isinstance(schema, dict) and dotted in schema:
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if found:
|
||||||
|
continue
|
||||||
# credentials.*: zuerst in defaults_applications prüfen, dann im Schema
|
# credentials.*: zuerst in defaults_applications prüfen, dann im Schema
|
||||||
if dotted.startswith('credentials.'):
|
if dotted.startswith('credentials.'):
|
||||||
key = dotted.split('.', 1)[1]
|
key = dotted.split('.', 1)[1]
|
||||||
|
@ -51,6 +51,61 @@ class TestGenerateDefaultApplications(unittest.TestCase):
|
|||||||
self.assertEqual(data["defaults_applications"]["testapp"]["foo"], "bar")
|
self.assertEqual(data["defaults_applications"]["testapp"]["foo"], "bar")
|
||||||
self.assertEqual(data["defaults_applications"]["testapp"]["baz"], 123)
|
self.assertEqual(data["defaults_applications"]["testapp"]["baz"], 123)
|
||||||
|
|
||||||
|
def test_missing_config_adds_empty_defaults(self):
|
||||||
|
"""
|
||||||
|
If a role has vars/main.yml but no config/main.yml,
|
||||||
|
the generator should still create an entry with an empty dict.
|
||||||
|
"""
|
||||||
|
# Create a role with vars/main.yml but without config/main.yml
|
||||||
|
role_no_config = self.roles_dir / "role-no-config"
|
||||||
|
(role_no_config / "vars").mkdir(parents=True)
|
||||||
|
(role_no_config / "vars" / "main.yml").write_text("application_id: noconfigapp\n")
|
||||||
|
|
||||||
|
# Run the generator
|
||||||
|
result = subprocess.run(
|
||||||
|
["python3", str(self.script_path),
|
||||||
|
"--roles-dir", str(self.roles_dir),
|
||||||
|
"--output-file", str(self.output_file)],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||||
|
|
||||||
|
# Verify the output YAML
|
||||||
|
data = yaml.safe_load(self.output_file.read_text())
|
||||||
|
apps = data.get("defaults_applications", {})
|
||||||
|
|
||||||
|
# The new application_id must exist and be an empty dict
|
||||||
|
self.assertIn("noconfigapp", apps)
|
||||||
|
self.assertEqual(apps["noconfigapp"], {})
|
||||||
|
|
||||||
|
def test_no_config_directory_adds_empty_defaults(self):
|
||||||
|
"""
|
||||||
|
If a role has vars/main.yml but no config directory at all,
|
||||||
|
the generator should still emit an empty-dict entry.
|
||||||
|
"""
|
||||||
|
# Create a role with vars/main.yml but do not create config/ at all
|
||||||
|
role_no_cfg_dir = self.roles_dir / "role-no-cfg-dir"
|
||||||
|
(role_no_cfg_dir / "vars").mkdir(parents=True)
|
||||||
|
(role_no_cfg_dir / "vars" / "main.yml").write_text("application_id: nocfgdirapp\n")
|
||||||
|
# Note: no config/ directory is created here
|
||||||
|
|
||||||
|
# Run the generator again
|
||||||
|
result = subprocess.run(
|
||||||
|
["python3", str(self.script_path),
|
||||||
|
"--roles-dir", str(self.roles_dir),
|
||||||
|
"--output-file", str(self.output_file)],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||||
|
|
||||||
|
# Load and inspect the output
|
||||||
|
data = yaml.safe_load(self.output_file.read_text())
|
||||||
|
apps = data.get("defaults_applications", {})
|
||||||
|
|
||||||
|
# Ensure that the application_id appears with an empty mapping
|
||||||
|
self.assertIn("nocfgdirapp", apps)
|
||||||
|
self.assertEqual(apps["nocfgdirapp"], {})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
71
tests/unit/filter_plugins/test_app_conf_extra_schema.py
Normal file
71
tests/unit/filter_plugins/test_app_conf_extra_schema.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from filter_plugins.get_app_conf import get_app_conf, AppConfigKeyError, ConfigEntryNotSetError
|
||||||
|
|
||||||
|
class TestGetAppConfFunctionality(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# Create a temp directory to simulate roles and schema files
|
||||||
|
self.tempdir = tempfile.TemporaryDirectory()
|
||||||
|
self.roles_dir = Path(self.tempdir.name) / "roles"
|
||||||
|
self.roles_dir.mkdir()
|
||||||
|
|
||||||
|
# application_id used in tests
|
||||||
|
self.app_id = "testapp"
|
||||||
|
# application dict missing the 'nested.key'
|
||||||
|
self.applications = {self.app_id: {}}
|
||||||
|
|
||||||
|
# Create schema directory and file for testapp
|
||||||
|
schema_dir = self.roles_dir / self.app_id / "schema"
|
||||||
|
schema_dir.mkdir(parents=True)
|
||||||
|
schema_file = schema_dir / "main.yml"
|
||||||
|
# Define a nested key in schema
|
||||||
|
schema_data = {
|
||||||
|
'nested': {
|
||||||
|
'key': ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with schema_file.open('w', encoding='utf-8') as f:
|
||||||
|
yaml.safe_dump(schema_data, f)
|
||||||
|
|
||||||
|
# Change cwd so get_app_conf finds roles/<app_id>/schema/main.yml
|
||||||
|
self.orig_cwd = os.getcwd()
|
||||||
|
os.chdir(self.tempdir.name)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
os.chdir(self.orig_cwd)
|
||||||
|
self.tempdir.cleanup()
|
||||||
|
|
||||||
|
def test_config_entry_not_set_error_raised(self):
|
||||||
|
# Attempt to access a key defined in schema but not set in applications
|
||||||
|
with self.assertRaises(ConfigEntryNotSetError):
|
||||||
|
get_app_conf(self.applications, self.app_id, 'nested.key', strict=True)
|
||||||
|
|
||||||
|
def test_strict_key_error_when_not_in_schema(self):
|
||||||
|
# Access a key not defined in schema nor in applications
|
||||||
|
with self.assertRaises(AppConfigKeyError):
|
||||||
|
get_app_conf(self.applications, self.app_id, 'undefined.path', strict=True)
|
||||||
|
|
||||||
|
def test_non_strict_returns_default(self):
|
||||||
|
# Non-strict mode should return default instead of raising
|
||||||
|
default_val = 'fallback'
|
||||||
|
result = get_app_conf(self.applications, self.app_id, 'nested.key', strict=False, default=default_val)
|
||||||
|
self.assertEqual(result, default_val)
|
||||||
|
|
||||||
|
def test_list_indexing_and_missing(self):
|
||||||
|
# Extend schema to define a list
|
||||||
|
schema_file = Path('roles') / self.app_id / 'schema' / 'main.yml'
|
||||||
|
schema_data = {'items': ['']}
|
||||||
|
with schema_file.open('w', encoding='utf-8') as f:
|
||||||
|
yaml.safe_dump(schema_data, f)
|
||||||
|
|
||||||
|
# Insert list into applications but leave index out of range
|
||||||
|
self.applications[self.app_id]['items'] = []
|
||||||
|
with self.assertRaises(AppConfigKeyError):
|
||||||
|
get_app_conf(self.applications, self.app_id, 'items[0]', strict=True)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
Loading…
x
Reference in New Issue
Block a user