Skip to content

Commit d108c5e

Browse files
authored
Merge pull request #73 from adamamyl/feature/quality-tooling
feat(quality): add ruff/mypy/bandit tooling and fix all issues
2 parents 210d3a4 + 74fb64b commit d108c5e

25 files changed

Lines changed: 1218 additions & 165 deletions

.github/workflows/quality.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
name: Quality
2+
on: [push, pull_request]
3+
jobs:
4+
quality:
5+
runs-on: ubuntu-latest
6+
steps:
7+
- uses: actions/checkout@v4
8+
- uses: astral-sh/setup-uv@v5
9+
- run: uv run pre-commit run --all-files

.pre-commit-config.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
repos:
2+
- repo: https://github.com/astral-sh/ruff-pre-commit
3+
rev: v0.11.13
4+
hooks:
5+
- id: ruff
6+
args: [--fix]
7+
- id: ruff-format
8+
9+
- repo: https://github.com/PyCQA/bandit
10+
rev: 1.8.5
11+
hooks:
12+
- id: bandit
13+
args: ["-c", "pyproject.toml"]
14+
15+
- repo: https://github.com/pre-commit/mirrors-mypy
16+
rev: v1.16.0
17+
hooks:
18+
- id: mypy
19+
additional_dependencies: ["types-requests"]
20+
21+
- repo: https://github.com/pre-commit/pre-commit-hooks
22+
rev: v5.0.0
23+
hooks:
24+
- id: check-json
25+
- id: check-yaml
26+
- id: check-added-large-files
27+
- id: detect-private-key
28+
- id: no-commit-to-branch
29+
args: [--branch, main]
30+
31+
- repo: https://github.com/betterleaks/betterleaks
32+
rev: v1.1.2
33+
hooks:
34+
- id: betterleaks

lib/executor.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import subprocess
22
import os
33
import sys
4-
from typing import List, Optional, Union, Any, Dict
4+
from typing import List, Optional, Union, Dict
55
from .logger import log
66
from . import constants
77

@@ -11,7 +11,9 @@ class Executor:
1111
Handles DRY_RUN, SUDO elevation, logging, and error checking.
1212
"""
1313

14-
def __init__(self, dry_run: bool = False, quiet: bool = False, verbose: bool = False, force: bool = False):
14+
def __init__(
15+
self, dry_run: bool = False, quiet: bool = False, verbose: bool = False, force: bool = False
16+
):
1517
self.dry_run = dry_run
1618
self.quiet = quiet
1719
self.verbose = verbose
@@ -31,9 +33,10 @@ def run(self,
3133
env: Optional[Dict[str, str]] = None,
3234
check: bool = True,
3335
run_quiet: bool = False,
34-
interactive: bool = False) -> subprocess.CompletedProcess:
36+
interactive: bool = False) -> subprocess.CompletedProcess[str]:
3537
"""
36-
Executes a shell command. If interactive=True, it allows direct terminal I/O (no pipe capture).
38+
Executes a shell command.
39+
If interactive=True, allows direct terminal I/O (no pipe capture).
3740
"""
3841

3942
if isinstance(command, str):
@@ -44,8 +47,7 @@ def run(self,
4447
log_cmd = " ".join(command)
4548

4649
if user:
47-
# Note: We are running the command as root, but providing user context via sudo -u
48-
# (although in the recursive call, the 'user' is handled by the recursive script's logic)
50+
# Running as root but delegating via sudo -u; recursive calls handle user context.
4951
if os.geteuid() != 0:
5052
log_cmd = f"sudo -H -u {user} {log_cmd}"
5153
cmd_list = ['sudo', '-H', '-u', user] + cmd_list
@@ -63,7 +65,7 @@ def run(self,
6365
if self.dry_run:
6466
if not suppress_logging:
6567
log.info(f"[DRY-RUN] {log_cmd}")
66-
return subprocess.CompletedProcess(args=cmd_list, returncode=0, stdout=b"", stderr=b"")
68+
return subprocess.CompletedProcess(args=cmd_list, returncode=0, stdout="", stderr="")
6769

6870
# --- 4. I/O Stream Determination ---
6971
if interactive:
@@ -123,10 +125,10 @@ def run(self,
123125
EXEC = Executor()
124126

125127

126-
def run_function_as_user(executor: Executor,
127-
user: str,
128-
function_name: str,
129-
*func_args: str) -> subprocess.CompletedProcess:
128+
def run_function_as_user(executor: Executor,
129+
user: str,
130+
function_name: str,
131+
*func_args: str) -> subprocess.CompletedProcess[str]:
130132
"""
131133
Executes a specific Python function (by name) from the main script as another user
132134
by recursively calling the setup script.

lib/installer_utils/apt_tools.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ def _is_package_installed(pkg: str) -> bool:
77
"""Checks if a package is installed using dpkg -s."""
88
try:
99
# Use subprocess.run directly as we don't need Executor for a simple check
10-
subprocess.run(['dpkg', '-s', pkg], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
10+
subprocess.run(
11+
['dpkg', '-s', pkg], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
12+
)
1113
return True
1214
except subprocess.CalledProcessError:
1315
return False
@@ -61,7 +63,7 @@ def apt_autoremove(exec_obj: Executor) -> None:
6163
exec_obj.run(autoremove_cmd, force_sudo=True)
6264

6365
def ensure_apt_repo(exec_obj: Executor, list_file: str, repo_line: str) -> None:
64-
"""Adds an apt repository line to a file if it is not already present, and deduplicates the file."""
66+
"""Adds an apt repository line to a file if not already present, and deduplicates."""
6567

6668
existing_lines = []
6769
try:

lib/installer_utils/git_tools.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import os
22
import subprocess
3-
from typing import Optional, List
3+
from typing import Optional
44
from ..executor import Executor
55
from ..logger import log
66
from ..constants import GIT_BIN_PATH
@@ -53,7 +53,9 @@ def clone_or_update_repo(exec_obj: Executor,
5353
if os.path.isdir(os.path.join(dest_dir, ".git")):
5454
try:
5555
# INTEGRITY CHECK: Use the resolved path constant (GIT_BIN_PATH)
56-
exec_obj.run([GIT_BIN_PATH, '-C', dest_dir, 'rev-parse', '--is-inside-work-tree'], user=user)
56+
exec_obj.run(
57+
[GIT_BIN_PATH, '-C', dest_dir, 'rev-parse', '--is-inside-work-tree'], user=user
58+
)
5759

5860
log.info(f"Updating existing repository: {dest_dir}")
5961

@@ -64,7 +66,9 @@ def clone_or_update_repo(exec_obj: Executor,
6466

6567
except Exception:
6668
# Integrity check or fetch failed -> Remove and re-clone
67-
log.warning(f"Repo integrity check or fetch failed at {dest_dir}; removing and recloning.")
69+
log.warning(
70+
f"Repo integrity check or fetch failed at {dest_dir}; removing and recloning."
71+
)
6872
exec_obj.run(f"rm -rf {dest_dir}", force_sudo=True)
6973

7074
# 4. Clone if missing or just removed
@@ -99,7 +103,10 @@ def clone_or_update_private_repo_with_key_check(exec_obj: Executor,
99103

100104
for attempt in range(MAX_CLONE_ATTEMPTS):
101105
try:
102-
log.info(f"Attempting to clone/update repository (Attempt {attempt + 1}/{MAX_CLONE_ATTEMPTS})...")
106+
log.info(
107+
f"Attempting to clone/update repository "
108+
f"(Attempt {attempt + 1}/{MAX_CLONE_ATTEMPTS})..."
109+
)
103110
clone_or_update_repo(
104111
exec_obj,
105112
repo_url,
@@ -151,7 +158,7 @@ def _configure_repo_ssh_key(exec_obj: Executor, user: str, repo_dir: str, key_pa
151158
log.info(f"Configuring Git SSH command for {repo_dir}")
152159

153160
# Git command to set the core.sshCommand locally
154-
# We use single quotes around the key_path in the ssh_command_value to protect it in the git config file
161+
# Single quotes around key_path protect spaces in the git config value.
155162
ssh_command_value = f"ssh -i '{key_path}' -o IdentitiesOnly=yes"
156163

157164
# --- FIX: Pass command as a single string and remove --local ---
@@ -167,12 +174,14 @@ def _configure_repo_ssh_key(exec_obj: Executor, user: str, repo_dir: str, key_pa
167174

168175
log.success(f"Set core.sshCommand to use {key_path} in {repo_dir}")
169176
except Exception as e:
170-
log.error(f"Failed to set local Git SSH config in {repo_dir}. Manual Git operations may fail.")
177+
log.error(
178+
f"Failed to set local Git SSH config in {repo_dir}. Manual Git operations may fail."
179+
)
171180
log.debug(f"Git config error: {e}")
172181

173182

174183
def set_homedir_perms_recursively(exec_obj: Executor, user: str, dir_path: str) -> None:
175-
"""Sets sane read/write permissions while preserving executable bits on files and directories."""
184+
"""Sets sane read/write permissions, preserving executable bits on files and directories."""
176185
log.info(f"Setting recursive permissions for {dir_path} owned by {user}")
177186

178187
# 1. Set ownership recursively
@@ -187,7 +196,7 @@ def set_homedir_perms_recursively(exec_obj: Executor, user: str, dir_path: str)
187196
exec_obj.run(f"chmod -R go-w {dir_path}", force_sudo=True)
188197

189198
# c) Crucial: Grant execute permission selectively (+X).
190-
# '+X' only grants execute if the item is a directory OR if it already has execute permissions set for any user.
199+
# '+X' grants execute only for directories or items already executable by any user.
191200
exec_obj.run(f"chmod -R a+X {dir_path}", force_sudo=True)
192201

193202

@@ -199,8 +208,7 @@ def set_ssh_perms(exec_obj: Executor, user: str, ssh_dir: str) -> None:
199208
exec_obj.run(f"chmod 700 {ssh_dir}", force_sudo=True)
200209
exec_obj.run(f"chown {user}:{user} {ssh_dir}", force_sudo=True)
201210

202-
# Note: We rely on _create_if_needed_ssh_key to enforce 600/644 on private/public keys.
203-
# We still ensure all files inside have correct ownership (if the keys were newly generated by root).
211+
# Re-fix ownership in case root generated the keys; _create_if_needed_ssh_key handles 600/644.
204212
exec_obj.run(f"chown {user}:{user} {ssh_dir}/* || true", force_sudo=True)
205213

206214
# We explicitly relax permissions on known_hosts and public keys to 644/400, just in case

lib/installer_utils/module_docker.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import shutil
22
import platform
33
import os
4-
from typing import List, Optional, Union, Dict
4+
from typing import List, Dict
55
import subprocess
66

77
from ..executor import Executor
@@ -36,7 +36,10 @@ def _remove_old_docker(exec_obj: Executor) -> None:
3636
log.info("Checking for and removing old/conflicting Docker packages...")
3737

3838
# Comprehensive query command to find packages that need removal
39-
query_cmd = "dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc | cut -f1"
39+
query_cmd = (
40+
"dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc "
41+
"podman-docker containerd runc | cut -f1"
42+
)
4043

4144
try:
4245
# Execute the query command as root to get the list of packages to remove
@@ -51,15 +54,17 @@ def _remove_old_docker(exec_obj: Executor) -> None:
5154
# 2. Construct the removal command
5255
remove_cmd = ["apt", "remove", "-y"] + packages_to_remove
5356

54-
log.warning(f"Removing the following old/conflicting packages: {', '.join(packages_to_remove)}")
57+
log.warning(
58+
f"Removing the following old/conflicting packages: {', '.join(packages_to_remove)}"
59+
)
5560

5661
# Execute the removal command
57-
# We allow check=False in case some packages are listed by dpkg but apt fails to find them, though unlikely here.
62+
# check=False: dpkg may list packages that apt can't find; we continue regardless.
5863
exec_obj.run(remove_cmd, force_sudo=True, check=True)
5964
log.success("Old Docker packages successfully removed.")
6065

6166
except Exception as e:
62-
log.warning(f"Failed to execute package removal query or removal. Continuing installation.")
67+
log.warning("Failed to execute package removal query or removal. Continuing installation.")
6368
log.debug(f"Removal error: {e}")
6469
# We don't halt here, as the subsequent installation step will fail if necessary.
6570

@@ -91,21 +96,25 @@ def install_docker_and_add_users(exec_obj: Executor, *users_to_add: str) -> None
9196
codename = os_info.get("VERSION_CODENAME") # e.g., 'noble' or 'bookworm'
9297

9398
if not os_id or not codename:
94-
log.critical("Could not detect OS ID or Codename from /etc/os-release. Aborting Docker setup.")
99+
log.critical(
100+
"Could not detect OS ID or Codename from /etc/os-release. Aborting Docker setup."
101+
)
95102
raise RuntimeError("Cannot proceed without distribution details.")
96103

97104
log.info(f"Detected OS: {os_id}, Codename: {codename}")
98105

99106
keyrings_dir = "/etc/apt/keyrings"
100-
docker_gpg_path = os.path.join(keyrings_dir, "docker.gpg") # Modern standard uses .gpg binary
107+
docker_gpg_path = os.path.join(keyrings_dir, "docker.gpg")
101108
list_file = "/etc/apt/sources.list.d/docker.list"
102109

103110
exec_obj.run(f"mkdir -p {keyrings_dir}", force_sudo=True)
104111

105112
if not os.path.exists(docker_gpg_path):
106113
log.info(f"Downloading and adding Docker GPG key for {os_id}.")
107-
# Note: We use gpg --dearmor to ensure a binary .gpg file for /etc/apt/keyrings compatibility
108-
curl_cmd = f"curl -fsSL https://download.docker.com/linux/{os_id}/gpg | gpg --dearmor -o {docker_gpg_path}"
114+
curl_cmd = (
115+
f"curl -fsSL https://download.docker.com/linux/{os_id}/gpg "
116+
f"| gpg --dearmor -o {docker_gpg_path}"
117+
)
109118
exec_obj.run(curl_cmd, force_sudo=True)
110119
# Ensure proper read permissions for apt
111120
exec_obj.run(f"chmod a+r {docker_gpg_path}", force_sudo=True)
@@ -122,7 +131,10 @@ def install_docker_and_add_users(exec_obj: Executor, *users_to_add: str) -> None
122131
display_arch = arch
123132

124133
# 2. Interpolate the correct ID, codename and arch into the repository line
125-
repo_line = f"deb [arch={display_arch} signed-by={docker_gpg_path}] https://download.docker.com/linux/{os_id} {codename} stable"
134+
repo_line = (
135+
f"deb [arch={display_arch} signed-by={docker_gpg_path}]"
136+
f" https://download.docker.com/linux/{os_id} {codename} stable"
137+
)
126138

127139
log.info(f"Using APT repository line: {repo_line}")
128140
ensure_apt_repo(exec_obj, list_file, repo_line)
@@ -144,7 +156,7 @@ def install_docker_and_add_users(exec_obj: Executor, *users_to_add: str) -> None
144156

145157
def _verify_docker_installation(exec_obj: Executor) -> None:
146158
"""
147-
Runs a simple Docker command (like 'docker run hello-world') and cleans up the resulting image/container.
159+
Runs 'docker info' and 'docker run hello-world' to verify installation, then cleans up.
148160
"""
149161
log.info("Running post-installation verification test...")
150162

@@ -212,7 +224,12 @@ def check_docker_volume_exists(exec_obj: Executor, volume_name: str) -> bool:
212224
log.info(f"Checking for existence of Docker volume: {volume_name}")
213225
try:
214226
# Use 'docker volume ls -q -f name=...' to check existence silently
215-
result = exec_obj.run(["docker", "volume", "ls", "-q", "-f", f"name=^{volume_name}$"], check=True, force_sudo=True, run_quiet=True)
227+
result = exec_obj.run(
228+
["docker", "volume", "ls", "-q", "-f", f"name=^{volume_name}$"],
229+
check=True,
230+
force_sudo=True,
231+
run_quiet=True,
232+
)
216233
if result.stdout.strip() == volume_name:
217234
log.success(f"Docker volume '{volume_name}' exists.")
218235
return True
@@ -222,7 +239,9 @@ def check_docker_volume_exists(exec_obj: Executor, volume_name: str) -> bool:
222239
log.error(f"Error checking Docker volumes: {e.stderr}")
223240
return False
224241

225-
def are_docker_services_running(exec_obj: Executor, user: str, cwd: str, service_names: List[str]) -> bool:
242+
def are_docker_services_running(
243+
exec_obj: Executor, user: str, cwd: str, service_names: List[str]
244+
) -> bool:
226245
"""
227246
Checks if a list of specific Docker Compose services are currently in the 'running' state.
228247
Requires running as the user that owns the compose stack.
@@ -255,7 +274,7 @@ def are_docker_services_running(exec_obj: Executor, user: str, cwd: str, service
255274
return all_running
256275

257276
except subprocess.CalledProcessError as e:
258-
log.warning(f"Failed to execute 'docker compose ps'. Stack may not exist.")
277+
log.warning("Failed to execute 'docker compose ps'. Stack may not exist.")
259278
log.debug(f"PS error: {e.stderr}")
260279
return False
261280
except Exception as e:

0 commit comments

Comments
 (0)