Skip to content

Commit b4c98b7

Browse files
committed
make capability requirements detect target OS implicitly
1 parent f6f3735 commit b4c98b7

5 files changed

Lines changed: 65 additions & 11 deletions

File tree

docs/plugins/linux-operations.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,10 @@ and `group.member_absent`. Removing a group member requires `confirm: true`.
239239

240240
## OS-aware job-scoped capability preflight
241241

242-
Automax can derive remote command dependencies from the selected job plan and render per-target checks. With `--detect-os`, Automax first reads `/etc/os-release` on each target, classifies it as DEBIAN-like or RHEL-like, filters backend-specific plugins for that OS family, and reports OS-mismatched plugins as skipped requirements instead of requiring irrelevant tools.
242+
Automax can derive remote command dependencies from the selected job plan and render per-target checks. Automax first reads `/etc/os-release` on each target, classifies it as DEBIAN-like or RHEL-like, filters backend-specific plugins for that OS family, and reports OS-mismatched plugins as skipped requirements instead of requiring irrelevant tools.
243243

244244
```bash
245-
automax capabilities requirements --job jobs/site.yaml --inventory inventory/prod.yaml --detect-os
245+
automax capabilities requirements --job jobs/site.yaml --inventory inventory/prod.yaml
246246
```
247247

248248
Normal `automax run` performs this OS detection and capability preflight implicitly before executing selected substeps. The older `--preflight-capabilities` flag remains accepted for compatibility, but the preflight is now the default for normal runs that require remote tools.

docs/plugins/security.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ paths.
103103

104104
## Capability preflight and redaction policy
105105

106-
Use `capabilities requirements --detect-os` to derive remote tool requirements from the selected job plan after detecting each target OS family. Normal `automax run` performs the same OS-aware capability preflight implicitly before executing remote tools. Use `capabilities install --sudo-password-env ENV_NAME` to install only missing dependency packages for the selected job and target OS.
106+
Use `capabilities requirements` to derive remote tool requirements from the selected job plan after detecting each target OS family. Normal `automax run` performs the same OS-aware capability preflight implicitly before executing remote tools. Use `capabilities install --sudo-password-env ENV_NAME` to install only missing dependency packages for the selected job and target OS.
107107

108108
The explicit capability plugins are:
109109

src/automax/cli/cli.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -977,7 +977,6 @@ def capabilities() -> None:
977977
@click.option("--tags", multiple=True, help="Show requirements for substeps matching one of these tags.")
978978
@click.option("--skip-tags", multiple=True, help="Hide requirements for substeps matching one of these tags.")
979979
@click.option("--plugin-path", multiple=True, help="External plugin file or directory.")
980-
@click.option("--detect-os", is_flag=True, help="Detect target OS and filter requirements for each OS family.")
981980
@click.option(
982981
"--format",
983982
"output_format",
@@ -997,7 +996,6 @@ def capability_requirements(
997996
tags: tuple[str, ...],
998997
skip_tags: tuple[str, ...],
999998
plugin_path: tuple[str, ...],
1000-
detect_os: bool,
1001999
output_format: str,
10021000
) -> None:
10031001
"""Render tool requirements derived from the selected job plan."""
@@ -1012,7 +1010,6 @@ def capability_requirements(
10121010
tags=_split_selectors(tags),
10131011
skip_tags=_split_selectors(skip_tags),
10141012
cli_vars=_parse_vars(cli_vars),
1015-
detect_os=detect_os,
10161013
)
10171014
except (AutomaxError, ValueError, RuntimeError) as exc:
10181015
raise click.ClickException(str(exc)) from exc

src/automax/core/engine.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -799,7 +799,6 @@ def capability_requirements_job(
799799
tags: Iterable[str] = (),
800800
skip_tags: Iterable[str] = (),
801801
cli_vars: Optional[Dict[str, Any]] = None,
802-
detect_os: bool = False,
803802
) -> Dict[str, Any]:
804803
"""Return job-scoped remote capability requirements from the selected plan."""
805804
resolved = self.resolve_job_context(
@@ -813,12 +812,11 @@ def capability_requirements_job(
813812
skip_tags=skip_tags,
814813
cli_vars=cli_vars,
815814
)
816-
os_by_target = self._detect_os_for_plan(resolved.plan, resolved.secrets) if detect_os else {}
815+
os_by_target = self._detect_os_for_plan(resolved.plan, resolved.secrets)
817816
requirements = collect_requirements(self.iter_rendered_plan_items(resolved, dry_run=True), os_by_target)
818817
return {
819818
"job": self._job_name(resolved.job),
820819
"mode": "capability-requirements",
821-
"detect_os": detect_os,
822820
"targets": [requirements[name] for name in sorted(requirements)],
823821
"target_count": len(requirements),
824822
"tool_count": len({tool for item in requirements.values() for tool in item["tools"]}),

tests/test_next_engine.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4408,7 +4408,9 @@ def test_fs_write_template_metadata_exposes_atomic_option():
44084408
assert "atomic" in params
44094409

44104410

4411-
def test_capability_requirements_are_derived_from_selected_job(tmp_path: Path):
4411+
def test_capability_requirements_are_derived_from_selected_job(tmp_path: Path, monkeypatch):
4412+
from automax.core.os_detect import TargetOS
4413+
44124414
job = write(
44134415
tmp_path / "job.yaml",
44144416
"""
@@ -4436,6 +4438,11 @@ def test_capability_requirements_are_derived_from_selected_job(tmp_path: Path):
44364438
""",
44374439
)
44384440
inventory = write(tmp_path / "inventory.yaml", "servers:\n controller:\n host: 127.0.0.1\n")
4441+
monkeypatch.setattr(
4442+
AutomaxEngine,
4443+
"_detect_os_for_plan",
4444+
lambda self, plan, secrets: {"controller": TargetOS(id="ubuntu", id_like=("debian",), family="debian", package_manager="apt")},
4445+
)
44394446

44404447
payload = AutomaxEngine().capability_requirements_job(job_path=str(job), inventory_path=str(inventory))
44414448

@@ -4493,6 +4500,58 @@ def test_capability_and_redaction_plugins_render_safe_previews():
44934500
assert not leaked.ok
44944501

44954502

4503+
4504+
def test_capability_requirements_cli_detects_os_without_flag(tmp_path: Path, monkeypatch):
4505+
from automax.core.os_detect import TargetOS
4506+
4507+
job = write(
4508+
tmp_path / "job.yaml",
4509+
"""
4510+
apiVersion: automax.io/v1
4511+
kind: Job
4512+
metadata:
4513+
name: cli-caps
4514+
tasks:
4515+
- id: ops
4516+
targets: all
4517+
steps:
4518+
- id: packages
4519+
substeps:
4520+
- id: install
4521+
use: pkg.install
4522+
with:
4523+
packages: [curl]
4524+
manager: auto
4525+
""",
4526+
)
4527+
inventory = write(tmp_path / "inventory.yaml", "servers:\n node:\n host: 127.0.0.1\n")
4528+
4529+
monkeypatch.setattr(
4530+
AutomaxEngine,
4531+
"_detect_os_for_plan",
4532+
lambda self, plan, secrets: {"node": TargetOS(id="ubuntu", id_like=("debian",), family="debian", package_manager="apt")},
4533+
)
4534+
4535+
result = CliRunner().invoke(
4536+
cli,
4537+
[
4538+
"capabilities",
4539+
"requirements",
4540+
"--job",
4541+
str(job),
4542+
"--inventory",
4543+
str(inventory),
4544+
"--format",
4545+
"json",
4546+
],
4547+
)
4548+
4549+
assert result.exit_code == 0, result.output
4550+
payload = json.loads(result.output)
4551+
assert payload["targets"][0]["os"]["family"] == "debian"
4552+
assert "apt-get" in payload["targets"][0]["tools"]
4553+
4554+
44964555
def test_capability_requirements_filter_tools_by_detected_os(tmp_path: Path, monkeypatch):
44974556
from automax.core.os_detect import TargetOS
44984557

@@ -4534,7 +4593,7 @@ def test_capability_requirements_filter_tools_by_detected_os(tmp_path: Path, mon
45344593
lambda plan, secrets: {"node": TargetOS(id="ubuntu", id_like=("debian",), family="debian", package_manager="apt")},
45354594
)
45364595

4537-
payload = engine.capability_requirements_job(job_path=str(job), inventory_path=str(inventory), detect_os=True)
4596+
payload = engine.capability_requirements_job(job_path=str(job), inventory_path=str(inventory))
45384597

45394598
target = payload["targets"][0]
45404599
assert target["os"]["family"] == "debian"

0 commit comments

Comments
 (0)