diff --git a/steward/ansible/sources.py b/steward/ansible/sources.py index a457d4d..e1b6855 100644 --- a/steward/ansible/sources.py +++ b/steward/ansible/sources.py @@ -1,6 +1,9 @@ from __future__ import annotations import asyncio import logging +import os +import stat +import tempfile from pathlib import Path logger = logging.getLogger(__name__) @@ -48,6 +51,7 @@ def get_sources(ansible_cfg: dict) -> list[dict]: "url": src.get("url"), "branch": src.get("branch", "main"), "pull_interval_seconds": int(src.get("pull_interval_seconds", 3600)), + "http_token": src.get("http_token", "") if src_type == "git" else "", }) return result @@ -99,27 +103,65 @@ def read_playbook(source_path: str, relative_path: str) -> str | None: async def git_pull(source: dict) -> None: - """Clone the git repo if absent; pull if already present.""" + """Clone the git repo if absent; pull if already present. + + When source['http_token'] is set, injects credentials via a temporary + GIT_ASKPASS script so they never touch .git/config or process args. + """ path = Path(source["path"]) - if not (path / ".git").exists(): - path.mkdir(parents=True, exist_ok=True) - proc = await asyncio.create_subprocess_exec( - "git", "clone", - "--branch", source["branch"], - "--single-branch", - source["url"], str(path), - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - _, stderr = await proc.communicate() - if proc.returncode != 0: - logger.error("git clone failed for %r: %s", source["name"], stderr.decode(errors="replace")) - else: - proc = await asyncio.create_subprocess_exec( - "git", "-C", str(path), "pull", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - _, stderr = await proc.communicate() - if proc.returncode != 0: - logger.error("git pull failed for %r: %s", source["name"], stderr.decode(errors="replace")) + token = source.get("http_token", "") + + env = None + askpass_path = None + if token: + fd, askpass_path = tempfile.mkstemp(prefix="steward-askpass-", suffix=".sh") + try: + script = ( + "#!/bin/sh\n" + 'case "$1" in\n' + ' Username*) echo "oauth2" ;;\n' + f' Password*) echo "{token}" ;;\n' + "esac\n" + ) + os.write(fd, script.encode()) + finally: + os.close(fd) + os.chmod(askpass_path, stat.S_IRWXU) + env = {**os.environ, "GIT_ASKPASS": askpass_path} + + try: + if not (path / ".git").exists(): + path.mkdir(parents=True, exist_ok=True) + proc = await asyncio.create_subprocess_exec( + "git", "clone", + "--branch", source["branch"], + "--single-branch", + source["url"], str(path), + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + env=env, + ) + _, stderr = await proc.communicate() + if proc.returncode != 0: + logger.error( + "git clone failed for %r: %s", + source["name"], + stderr.decode(errors="replace"), + ) + else: + proc = await asyncio.create_subprocess_exec( + "git", "-C", str(path), "pull", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + env=env, + ) + _, stderr = await proc.communicate() + if proc.returncode != 0: + logger.error( + "git pull failed for %r: %s", + source["name"], + stderr.decode(errors="replace"), + ) + finally: + if askpass_path and os.path.exists(askpass_path): + os.unlink(askpass_path) diff --git a/steward/settings/routes.py b/steward/settings/routes.py index 4414031..805b5a1 100644 --- a/steward/settings/routes.py +++ b/steward/settings/routes.py @@ -226,6 +226,9 @@ async def ansible_add_source(): entry["pull_interval_seconds"] = int(form.get("pull_interval_seconds", 3600)) except ValueError: entry["pull_interval_seconds"] = 3600 + token = form.get("http_token", "").strip() + if token: + entry["http_token"] = token else: entry["path"] = form.get("path", "").strip() sources = await _get_ansible_sources() @@ -263,6 +266,12 @@ async def ansible_save_source(idx: int): entry["pull_interval_seconds"] = int(form.get("pull_interval_seconds", 3600)) except ValueError: entry["pull_interval_seconds"] = 3600 + token = form.get("http_token", "").strip() + if token: + entry["http_token"] = token + elif "http_token" in sources[idx]: + # Preserve existing token — password fields don't round-trip via browser + entry["http_token"] = sources[idx]["http_token"] else: entry["path"] = form.get("path", "").strip() sources[idx] = entry diff --git a/steward/templates/settings/_ansible_sources.html b/steward/templates/settings/_ansible_sources.html index ba47f76..610102c 100644 --- a/steward/templates/settings/_ansible_sources.html +++ b/steward/templates/settings/_ansible_sources.html @@ -94,6 +94,15 @@ +
+ + +
Pull interval (seconds)
+
+ + +