Add support for detecting flatpaks and snaps
This commit is contained in:
parent
11351cce87
commit
eb1d096c90
10 changed files with 2033 additions and 16 deletions
|
|
@ -312,3 +312,185 @@ def test_parse_group_handles_short_lines(tmp_path: Path):
|
|||
assert 1000 in gid_to_name
|
||||
assert 1001 not in gid_to_name # skipped due to short line
|
||||
assert 1002 in gid_to_name
|
||||
|
||||
|
||||
def test_find_flatpaks_in_root_detects_remote_branch_and_arch(tmp_path: Path):
|
||||
import enroll.accounts as a
|
||||
|
||||
root = tmp_path / "flatpak"
|
||||
(root / "repo").mkdir(parents=True)
|
||||
(root / "repo" / "config").write_text(
|
||||
'[remote "acme"]\nurl=https://flatpak.example/repo/\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
ref = (
|
||||
root
|
||||
/ "repo"
|
||||
/ "refs"
|
||||
/ "remotes"
|
||||
/ "acme"
|
||||
/ "app"
|
||||
/ "com.example.App"
|
||||
/ "x86_64"
|
||||
/ "stable"
|
||||
)
|
||||
ref.parent.mkdir(parents=True)
|
||||
ref.write_text("checksum\n", encoding="utf-8")
|
||||
active = root / "app" / "com.example.App" / "x86_64" / "stable" / "active"
|
||||
active.mkdir(parents=True)
|
||||
|
||||
remotes = a.find_flatpak_remotes(str(root), method="system")
|
||||
assert [(r.name, r.url, r.method) for r in remotes] == [
|
||||
("acme", "https://flatpak.example/repo/", "system")
|
||||
]
|
||||
|
||||
apps = a._find_flatpaks_in_root(str(root), method="system")
|
||||
assert len(apps) == 1
|
||||
assert apps[0].name == "com.example.App"
|
||||
assert apps[0].remote == "acme"
|
||||
assert apps[0].branch == "stable"
|
||||
assert apps[0].arch == "x86_64"
|
||||
|
||||
|
||||
def test_parse_snap_list_output_detects_channel_revision_and_modes():
|
||||
import enroll.accounts as a
|
||||
|
||||
output = """Name Version Rev Tracking Publisher Notes
|
||||
code abc 123 latest/stable vscode✓ classic
|
||||
mydev 1.0 42 latest/edge example devmode,dangerous
|
||||
bare 1.0 5 latest/stable canonical✓ base
|
||||
"""
|
||||
|
||||
snaps = {snap.name: snap for snap in a._parse_snap_list_output(output)}
|
||||
assert snaps["code"].channel == "latest/stable"
|
||||
assert snaps["code"].revision == 123
|
||||
assert snaps["code"].classic is True
|
||||
assert snaps["mydev"].devmode is True
|
||||
assert snaps["mydev"].dangerous is True
|
||||
assert snaps["bare"].notes == ["base"]
|
||||
|
||||
|
||||
def test_parse_flatpak_list_output_detects_system_refs():
|
||||
from enroll.accounts import _parse_flatpak_list_output
|
||||
|
||||
output = "\n".join(
|
||||
[
|
||||
"app/org.example.App/x86_64/stable\tflathub\tstable\tx86_64",
|
||||
"runtime/org.freedesktop.Platform/x86_64/24.08\tflathub\t24.08\tx86_64",
|
||||
]
|
||||
)
|
||||
|
||||
refs = _parse_flatpak_list_output(
|
||||
output, method="system", columns=("ref", "origin", "branch", "arch")
|
||||
)
|
||||
|
||||
assert [(r.kind, r.name, r.remote, r.branch, r.arch) for r in refs] == [
|
||||
("app", "org.example.App", "flathub", "stable", "x86_64"),
|
||||
("runtime", "org.freedesktop.Platform", "flathub", "24.08", "x86_64"),
|
||||
]
|
||||
assert refs[0].source == "flatpak-list"
|
||||
|
||||
|
||||
def test_find_system_flatpaks_prefers_flatpak_list(monkeypatch):
|
||||
import subprocess
|
||||
import enroll.accounts as a
|
||||
|
||||
calls = []
|
||||
|
||||
def fake_run(args, **kwargs):
|
||||
calls.append(args)
|
||||
if args == ["flatpak", "list", "--columns=help"]:
|
||||
return subprocess.CompletedProcess(
|
||||
args,
|
||||
0,
|
||||
stdout="application\norigin\nbranch\narch\n",
|
||||
stderr="",
|
||||
)
|
||||
return subprocess.CompletedProcess(
|
||||
args,
|
||||
0,
|
||||
stdout="app/org.example.App/x86_64/stable\tacme\tstable\tx86_64\n",
|
||||
stderr="",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(a.shutil, "which", lambda cmd: "/usr/bin/flatpak")
|
||||
monkeypatch.setattr(a.subprocess, "run", fake_run)
|
||||
monkeypatch.setattr(
|
||||
a,
|
||||
"_find_flatpaks_in_root",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("fallback used")),
|
||||
)
|
||||
|
||||
refs = a.find_system_flatpaks()
|
||||
|
||||
assert calls[0] == ["flatpak", "list", "--columns=help"]
|
||||
assert calls[1][:3] == ["flatpak", "list", "--system"]
|
||||
assert refs[0].name == "org.example.App"
|
||||
assert refs[0].method == "system"
|
||||
assert refs[0].remote == "acme"
|
||||
|
||||
|
||||
def test_parse_flatpak_list_output_detects_application_columns():
|
||||
from enroll.accounts import _parse_flatpak_list_output
|
||||
|
||||
output = "org.example.App\tflathub\tstable\tx86_64\n"
|
||||
refs = _parse_flatpak_list_output(
|
||||
output, method="system", columns=("application", "origin", "branch", "arch")
|
||||
)
|
||||
|
||||
assert len(refs) == 1
|
||||
assert refs[0].name == "org.example.App"
|
||||
assert refs[0].kind is None
|
||||
assert refs[0].remote == "flathub"
|
||||
assert refs[0].branch == "stable"
|
||||
assert refs[0].arch == "x86_64"
|
||||
|
||||
|
||||
def test_parse_plain_flatpak_list_output_like_default_table():
|
||||
from enroll.accounts import _parse_flatpak_list_output
|
||||
|
||||
output = """Name Application ID Version Branch Installation
|
||||
Mesa org.freedesktop.Platform.GL.default 26.0.6 25.08 system
|
||||
Mesa (Extra) org.freedesktop.Platform.GL.default 26.0.6 25.08-extra system
|
||||
Codecs Extra Extension org.freedesktop.Platform.codecs-extra 25.08-extra system
|
||||
KDE Application Platform org.kde.Platform 6.10 system
|
||||
OnionShare org.onionshare.OnionShare 2.6.4 stable system
|
||||
"""
|
||||
|
||||
refs = _parse_flatpak_list_output(output, method="system", columns=None)
|
||||
by_name_branch = {(r.name, r.branch) for r in refs}
|
||||
|
||||
assert ("org.onionshare.OnionShare", "stable") in by_name_branch
|
||||
assert ("org.freedesktop.Platform.GL.default", "25.08") in by_name_branch
|
||||
assert ("org.freedesktop.Platform.GL.default", "25.08-extra") in by_name_branch
|
||||
assert ("org.kde.Platform", "6.10") in by_name_branch
|
||||
|
||||
|
||||
def test_parse_flatpak_columns_help_handles_description_table():
|
||||
from enroll.accounts import _parse_flatpak_columns_help
|
||||
|
||||
output = """
|
||||
Available columns:
|
||||
application The application ID
|
||||
branch The branch
|
||||
installation The installation
|
||||
"""
|
||||
|
||||
assert _parse_flatpak_columns_help(output) >= {
|
||||
"application",
|
||||
"branch",
|
||||
"installation",
|
||||
}
|
||||
|
||||
|
||||
def test_flatpak_list_attempts_respect_supported_columns():
|
||||
from enroll.accounts import _flatpak_list_attempts
|
||||
|
||||
attempts = _flatpak_list_attempts(
|
||||
"--system", {"application", "branch", "installation"}
|
||||
)
|
||||
command_strings = [" ".join(args) for args, _columns in attempts]
|
||||
|
||||
assert any("--columns=application,branch" in cmd for cmd in command_strings)
|
||||
assert not any("origin" in cmd for cmd in command_strings)
|
||||
assert command_strings[-1] == "flatpak list --system"
|
||||
|
|
|
|||
|
|
@ -224,6 +224,19 @@ def test_harvest_dedup_manual_packages_and_builds_etc_custom(
|
|||
|
||||
monkeypatch.setattr(harvest, "collect_non_system_users", lambda: [])
|
||||
|
||||
import enroll.accounts as accounts
|
||||
|
||||
monkeypatch.setattr(accounts, "find_system_flatpaks", lambda: [])
|
||||
monkeypatch.setattr(accounts, "find_system_flatpak_remotes", lambda: [])
|
||||
monkeypatch.setattr(
|
||||
accounts, "find_user_flatpak_remotes", lambda home, user=None: []
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
accounts,
|
||||
"find_system_snaps",
|
||||
lambda: [accounts.SnapInstall(name="code", channel="latest/stable")],
|
||||
)
|
||||
|
||||
def fake_stat_triplet(p: str):
|
||||
if p == "/usr/local/bin/myscript":
|
||||
return ("root", "root", "0755")
|
||||
|
|
@ -259,6 +272,9 @@ def test_harvest_dedup_manual_packages_and_builds_etc_custom(
|
|||
for o in openvpn_obs
|
||||
)
|
||||
|
||||
assert st["roles"]["snap"]["role_name"] == "snap"
|
||||
assert st["roles"]["snap"]["system_snaps"][0]["name"] == "code"
|
||||
|
||||
# Service role captured modified conffile
|
||||
svc = st["roles"]["services"][0]
|
||||
assert svc["unit"] == "openvpn.service"
|
||||
|
|
|
|||
|
|
@ -286,3 +286,20 @@ def test_collect_firewall_runtime_snapshot_is_per_family_fallback(
|
|||
assert (
|
||||
tmp_path / "artifacts" / "firewall_runtime" / "firewall" / "iptables.v6"
|
||||
).exists()
|
||||
|
||||
|
||||
def test_package_role_names_do_not_collide_with_singleton_roles():
|
||||
from enroll.harvest import _role_name_from_pkg
|
||||
|
||||
assert _role_name_from_pkg("flatpak") == "package_flatpak"
|
||||
assert _role_name_from_pkg("snap") == "package_snap"
|
||||
assert _role_name_from_pkg("users") == "package_users"
|
||||
assert _role_name_from_pkg("nginx") == "nginx"
|
||||
|
||||
|
||||
def test_service_role_names_do_not_collide_with_singleton_roles():
|
||||
from enroll.harvest import _role_name_from_unit
|
||||
|
||||
assert _role_name_from_unit("flatpak.service") == "service_flatpak"
|
||||
assert _role_name_from_unit("users.service") == "service_users"
|
||||
assert _role_name_from_unit("nginx.service") == "nginx"
|
||||
|
|
|
|||
|
|
@ -1064,3 +1064,317 @@ def test_render_firewall_runtime_tasks_with_ipv6():
|
|||
}
|
||||
result = manifest._render_firewall_runtime_tasks(state)
|
||||
assert len(result) >= 1
|
||||
|
||||
|
||||
def test_manifest_renders_flatpak_and_snap_details(tmp_path: Path):
|
||||
bundle = tmp_path / "bundle"
|
||||
out = tmp_path / "ansible"
|
||||
state = {
|
||||
"schema_version": 3,
|
||||
"host": {"hostname": "test", "os": "debian", "pkg_backend": "dpkg"},
|
||||
"inventory": {"packages": {}},
|
||||
"roles": {
|
||||
"users": {
|
||||
"role_name": "users",
|
||||
"users": [
|
||||
{
|
||||
"name": "alice",
|
||||
"uid": 1000,
|
||||
"gid": 1000,
|
||||
"gecos": "Alice",
|
||||
"home": "/home/alice",
|
||||
"shell": "/bin/bash",
|
||||
"primary_group": "alice",
|
||||
"supplementary_groups": [],
|
||||
}
|
||||
],
|
||||
"managed_files": [],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
"user_flatpak_remotes": [
|
||||
{
|
||||
"name": "acme-user",
|
||||
"method": "user",
|
||||
"url": "https://flatpak.example/user-repo/",
|
||||
"user": "alice",
|
||||
"home": "/home/alice",
|
||||
},
|
||||
],
|
||||
"user_flatpaks": {
|
||||
"alice": [
|
||||
{
|
||||
"name": "org.example.UserApp",
|
||||
"method": "user",
|
||||
"remote": "acme-user",
|
||||
"branch": "stable",
|
||||
"arch": "x86_64",
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
"flatpak": {
|
||||
"role_name": "flatpak",
|
||||
"remotes": [
|
||||
{
|
||||
"name": "acme",
|
||||
"method": "system",
|
||||
"url": "https://flatpak.example/repo/",
|
||||
},
|
||||
],
|
||||
"system_flatpaks": [
|
||||
{
|
||||
"name": "com.example.App",
|
||||
"method": "system",
|
||||
"remote": "acme",
|
||||
"branch": "stable",
|
||||
"arch": "x86_64",
|
||||
}
|
||||
],
|
||||
"notes": [],
|
||||
},
|
||||
"snap": {
|
||||
"role_name": "snap",
|
||||
"system_snaps": [
|
||||
{
|
||||
"name": "code",
|
||||
"channel": "latest/stable",
|
||||
"revision": 123,
|
||||
"classic": True,
|
||||
"notes": ["classic"],
|
||||
}
|
||||
],
|
||||
"notes": [],
|
||||
},
|
||||
"services": [],
|
||||
"packages": [],
|
||||
"apt_config": {
|
||||
"role_name": "apt_config",
|
||||
"managed_files": [],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
},
|
||||
"dnf_config": {
|
||||
"role_name": "dnf_config",
|
||||
"managed_files": [],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
},
|
||||
"etc_custom": {
|
||||
"role_name": "etc_custom",
|
||||
"managed_files": [],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
},
|
||||
"usr_local_custom": {
|
||||
"role_name": "usr_local_custom",
|
||||
"managed_files": [],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
},
|
||||
"extra_paths": {
|
||||
"role_name": "extra_paths",
|
||||
"include_patterns": [],
|
||||
"exclude_patterns": [],
|
||||
"managed_files": [],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
},
|
||||
},
|
||||
}
|
||||
bundle.mkdir(parents=True, exist_ok=True)
|
||||
(bundle / "state.json").write_text(json.dumps(state, indent=2), encoding="utf-8")
|
||||
|
||||
manifest.manifest(str(bundle), str(out))
|
||||
|
||||
users_defaults = (out / "roles" / "users" / "defaults" / "main.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
users_tasks = (out / "roles" / "users" / "tasks" / "main.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
users_readme = (out / "roles" / "users" / "README.md").read_text(encoding="utf-8")
|
||||
flatpak_defaults = (out / "roles" / "flatpak" / "defaults" / "main.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
flatpak_tasks = (out / "roles" / "flatpak" / "tasks" / "main.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
snap_defaults = (out / "roles" / "snap" / "defaults" / "main.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
snap_tasks = (out / "roles" / "snap" / "tasks" / "main.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert "users_flatpak_remotes:" in users_defaults
|
||||
assert "remote: acme-user" in users_defaults
|
||||
assert "community.general.snap" not in users_tasks
|
||||
assert "Install system-wide snaps" not in users_tasks
|
||||
assert "Install system-wide Flatpaks" not in users_tasks
|
||||
assert "ansible-galaxy collection install -r requirements.yml" in users_readme
|
||||
|
||||
assert "snap_system_snaps:" in snap_defaults
|
||||
assert "channel: latest/stable" in snap_defaults
|
||||
assert "classic: true" in snap_defaults
|
||||
assert "community.general.snap" in snap_tasks
|
||||
assert "Install system-wide snaps with full detected attributes" in snap_tasks
|
||||
assert "Install system-wide snaps with compatibility options" in snap_tasks
|
||||
assert "Install system-wide snaps with minimal options" in snap_tasks
|
||||
assert "ignore_errors: true" in snap_tasks
|
||||
|
||||
assert "flatpak_system_flatpaks:" in flatpak_defaults
|
||||
assert "remote: acme" in flatpak_defaults
|
||||
assert "community.general.flatpak" in flatpak_tasks
|
||||
assert "Install system-wide Flatpaks" in flatpak_tasks
|
||||
assert (out / "requirements.yml").exists()
|
||||
|
||||
|
||||
def test_users_role_without_portable_apps_omits_community_general_tasks(tmp_path):
|
||||
bundle = tmp_path / "bundle"
|
||||
out = tmp_path / "out"
|
||||
state = {
|
||||
"roles": {
|
||||
"users": {
|
||||
"role_name": "users",
|
||||
"users": [
|
||||
{
|
||||
"name": "alice",
|
||||
"uid": 1000,
|
||||
"gid": 1000,
|
||||
"gecos": "Alice",
|
||||
"home": "/home/alice",
|
||||
"shell": "/bin/bash",
|
||||
"primary_group": "alice",
|
||||
"supplementary_groups": [],
|
||||
}
|
||||
],
|
||||
"managed_files": [],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
},
|
||||
"services": [],
|
||||
"packages": [],
|
||||
},
|
||||
}
|
||||
bundle.mkdir(parents=True, exist_ok=True)
|
||||
(bundle / "state.json").write_text(json.dumps(state, indent=2), encoding="utf-8")
|
||||
|
||||
manifest.manifest(str(bundle), str(out))
|
||||
|
||||
users_tasks = (out / "roles" / "users" / "tasks" / "main.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
users_meta = (out / "roles" / "users" / "meta" / "main.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert "community.general.flatpak" not in users_tasks
|
||||
assert "community.general.snap" not in users_tasks
|
||||
assert "collections:" not in users_meta
|
||||
|
||||
|
||||
def test_manifest_emits_flatpak_role_even_when_no_flatpaks(tmp_path):
|
||||
bundle = tmp_path / "bundle"
|
||||
out = tmp_path / "out"
|
||||
state = {
|
||||
"roles": {
|
||||
"users": {
|
||||
"role_name": "users",
|
||||
"users": [],
|
||||
"managed_files": [],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
"user_flatpaks": {},
|
||||
"user_flatpak_remotes": [],
|
||||
},
|
||||
"flatpak": {
|
||||
"role_name": "flatpak",
|
||||
"system_flatpaks": [],
|
||||
"remotes": [],
|
||||
"notes": [],
|
||||
},
|
||||
"services": [],
|
||||
"packages": [],
|
||||
}
|
||||
}
|
||||
bundle.mkdir(parents=True, exist_ok=True)
|
||||
(bundle / "state.json").write_text(json.dumps(state, indent=2), encoding="utf-8")
|
||||
|
||||
manifest.manifest(str(bundle), str(out))
|
||||
|
||||
flatpak_tasks = (out / "roles" / "flatpak" / "tasks" / "main.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
flatpak_defaults = (out / "roles" / "flatpak" / "defaults" / "main.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert "flatpak_system_flatpaks: []" in flatpak_defaults
|
||||
assert "flatpak_remotes: []" in flatpak_defaults
|
||||
assert "Install system-wide Flatpaks" in flatpak_tasks
|
||||
assert "Ensure system Flatpak remotes exist" in flatpak_tasks
|
||||
|
||||
|
||||
def test_manifest_avoids_package_role_collision_with_flatpak_singleton(tmp_path):
|
||||
bundle = tmp_path / "bundle"
|
||||
out = tmp_path / "out"
|
||||
state = {
|
||||
"roles": {
|
||||
"users": {
|
||||
"role_name": "users",
|
||||
"users": [],
|
||||
"managed_files": [],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
"user_flatpaks": {},
|
||||
"user_flatpak_remotes": [],
|
||||
},
|
||||
"flatpak": {
|
||||
"role_name": "flatpak",
|
||||
"remotes": [
|
||||
{
|
||||
"name": "flathub",
|
||||
"method": "system",
|
||||
"url": "https://dl.flathub.org/repo/",
|
||||
}
|
||||
],
|
||||
"system_flatpaks": [
|
||||
{
|
||||
"name": "org.onionshare.OnionShare",
|
||||
"method": "system",
|
||||
"remote": "flathub",
|
||||
"branch": "stable",
|
||||
"arch": "x86_64",
|
||||
}
|
||||
],
|
||||
"notes": [],
|
||||
},
|
||||
"services": [],
|
||||
"packages": [
|
||||
{
|
||||
"package": "flatpak",
|
||||
"role_name": "flatpak",
|
||||
"managed_files": [],
|
||||
"managed_dirs": [],
|
||||
"managed_links": [],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
"has_config": True,
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
bundle.mkdir(parents=True, exist_ok=True)
|
||||
(bundle / "state.json").write_text(json.dumps(state, indent=2), encoding="utf-8")
|
||||
|
||||
manifest.manifest(str(bundle), str(out))
|
||||
|
||||
flatpak_defaults = (out / "roles" / "flatpak" / "defaults" / "main.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
playbook = (out / "playbook.yml").read_text(encoding="utf-8")
|
||||
|
||||
assert "org.onionshare.OnionShare" in flatpak_defaults
|
||||
assert (out / "roles" / "package_flatpak" / "tasks" / "main.yml").exists()
|
||||
assert "role: flatpak" in playbook
|
||||
assert "role: package_flatpak" in playbook
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue