feat(ansible): GIT_ASKPASS per-source HTTP token auth + settings UI

This commit is contained in:
2026-06-05 18:48:00 -04:00
parent ea47169049
commit d906232eb2
4 changed files with 232 additions and 23 deletions
+65 -23
View File
@@ -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)