Add ability to run cargo aoc under docker

This commit is contained in:
Bill Thiede 2025-12-04 16:49:51 -08:00
parent 88ec3631ab
commit 492d06b0d6
2 changed files with 147 additions and 16 deletions

View File

@ -85,6 +85,18 @@ class Config:
@property
def rsync_config(self) -> Optional[dict]:
return self.config.get('rsync')
@property
def docker_config(self) -> dict:
"""Get Docker configuration with defaults"""
docker_config = self.config.get('docker', {})
return {
'build_cache_dir': docker_config.get('build_cache_dir', ''),
'registry_cache_dir': docker_config.get('registry_cache_dir', ''),
'memory': docker_config.get('memory', '2g'),
'cpus': docker_config.get('cpus', '2'),
'image': docker_config.get('image', 'rust:latest')
}
class Database:
@ -560,10 +572,107 @@ class CargoAOCRunner:
logger.warning(f"Could not get recent commits for {repo_path}: {e}")
return commits
@staticmethod
def _run_cargo_aoc_in_container(work_dir: Path, day: int, repo_root: Path, docker_config: dict) -> subprocess.CompletedProcess:
"""Run cargo aoc in a Docker container for security
Args:
work_dir: Working directory (year directory) - can be absolute or relative
day: Day number to run
repo_root: Absolute path to repository root
docker_config: Docker configuration dictionary
Returns:
CompletedProcess with stdout, stderr, returncode
"""
repo_root = Path(repo_root).resolve()
work_dir = Path(work_dir).resolve()
# Ensure work_dir is under repo_root
try:
work_dir_rel = str(work_dir.relative_to(repo_root))
except ValueError:
# If work_dir is not under repo_root, this is an error
raise ValueError(f"work_dir {work_dir} is not under repo_root {repo_root}")
# Determine build cache directory
build_cache_dir = docker_config.get('build_cache_dir', '')
use_temp_build = False
temp_build_dir = None
if build_cache_dir:
# Use persistent build cache directory
build_cache_path = Path(build_cache_dir).resolve()
build_cache_path.mkdir(parents=True, exist_ok=True)
logger.info(f"Using persistent build cache: {build_cache_path}")
else:
# Create a temporary directory for cargo build artifacts (outside repo)
import tempfile
temp_build_dir = tempfile.mkdtemp(prefix='cargo-aoc-build-')
build_cache_path = Path(temp_build_dir)
use_temp_build = True
logger.info(f"Using temporary build cache: {build_cache_path}")
# Determine registry cache directory
registry_cache_dir = docker_config.get('registry_cache_dir', '')
try:
# Build Docker command
docker_cmd = [
'docker', 'run',
'--rm', # Remove container after execution
'--network=none', # No network access
'--memory', docker_config.get('memory', '2g'), # Limit memory
'--cpus', str(docker_config.get('cpus', '2')), # Limit CPU
'--read-only', # Read-only root filesystem
'--tmpfs', '/tmp:rw,noexec,nosuid,size=1g', # Writable /tmp for cargo
'-v', f'{repo_root}:/repo:ro', # Mount repo read-only
'-v', f'{build_cache_path}:/build:rw', # Writable build directory
'-w', f'/repo/{work_dir_rel}', # Working directory in container
]
# Handle cargo registry cache
if registry_cache_dir:
# Use persistent registry cache
registry_cache_path = Path(registry_cache_dir).resolve()
registry_cache_path.mkdir(parents=True, exist_ok=True)
docker_cmd.extend(['-v', f'{registry_cache_path}:/root/.cargo/registry:rw'])
logger.info(f"Using persistent registry cache: {registry_cache_path}")
else:
# Use tmpfs for registry cache (cleared after each run)
docker_cmd.extend(['--tmpfs', '/root/.cargo/registry:rw,noexec,nosuid,size=100m'])
# Add Docker image and command
docker_cmd.extend([
docker_config.get('image', 'rust:latest'),
'cargo', 'aoc', '--day', str(day)
])
# Set CARGO_TARGET_DIR to use the mounted build directory
env = os.environ.copy()
env['CARGO_TARGET_DIR'] = '/build/target'
result = subprocess.run(
docker_cmd,
capture_output=True,
text=True,
timeout=300, # 5 minute timeout
env=env
)
return result
finally:
# Clean up temporary build directory if we created one
if use_temp_build and temp_build_dir:
try:
shutil.rmtree(temp_build_dir)
except Exception as e:
logger.warning(f"Failed to clean up temp build directory {temp_build_dir}: {e}")
@staticmethod
def run_benchmarks(repo_path: Path, year: int, user: str = "unknown",
repo_url: str = "", is_multi_year: bool = False,
log_file: Optional[Path] = None) -> List[PerformanceResult]:
log_file: Optional[Path] = None, docker_config: Optional[dict] = None) -> List[PerformanceResult]:
"""Run cargo aoc benchmarks and parse results
Args:
@ -575,17 +684,19 @@ class CargoAOCRunner:
log_file: Optional path to log file to append cargo aoc output to
"""
results = []
repo_path = Path(repo_path)
repo_path = Path(repo_path).resolve()
# Get git revision
git_rev = CargoAOCRunner.get_git_rev(repo_path)
# Determine the working directory
# Determine the working directory and repo root
if is_multi_year:
# For multi-year repos, repo_path is already the year directory
work_dir = repo_path
repo_root = repo_path # For multi-year, repo_path is the repo root
else:
# For single repos, check if we need to navigate to a year subdirectory
repo_root = repo_path # Repo root is the repo_path
work_dir = repo_path
year_dir = repo_path / str(year)
if year_dir.exists() and year_dir.is_dir():
@ -603,17 +714,18 @@ class CargoAOCRunner:
for day in days:
try:
logger.info(f"Running cargo aoc for {user} year {year} day {day} in {work_dir}")
# Run cargo aoc for this day (no year flag, must be in correct directory)
cmd = ['cargo', 'aoc', '--day', str(day)]
result = subprocess.run(
cmd,
cwd=work_dir,
capture_output=True,
text=True,
timeout=300 # 5 minute timeout per day
)
logger.info(f"Running cargo aoc for {user} year {year} day {day} in {work_dir} (in Docker container)")
# Run cargo aoc in a Docker container for security
# Use default docker_config if not provided
if docker_config is None:
docker_config = {
'build_cache_dir': '',
'registry_cache_dir': '',
'memory': '2g',
'cpus': '2',
'image': 'rust:latest'
}
result = CargoAOCRunner._run_cargo_aoc_in_container(work_dir, day, repo_root, docker_config)
# Write to log file if provided
if log_file:
@ -625,7 +737,7 @@ class CargoAOCRunner:
with open(log_file, 'a', encoding='utf-8') as f:
f.write(f"\n{'='*80}\n")
f.write(f"[{timestamp}] {user} - Year {year} - Day {day}\n")
f.write(f"Command: {' '.join(cmd)}\n")
f.write(f"Command: cargo aoc --day {day} (in Docker container)\n")
f.write(f"Working Directory: {work_dir}\n")
f.write(f"Return Code: {result.returncode}\n")
f.write(f"{'='*80}\n")
@ -1956,9 +2068,11 @@ class AOCSync:
# Create log file path in output directory
log_file = Path(self.config.output_dir) / 'cargo-aoc.log'
log_file.parent.mkdir(parents=True, exist_ok=True)
# Get Docker configuration
docker_config = self.config.docker_config
results = CargoAOCRunner.run_benchmarks(repo_path, year=year, user=user,
repo_url=repo_url, is_multi_year=is_multi_year,
log_file=log_file)
log_file=log_file, docker_config=docker_config)
# Store results
for result in results:

View File

@ -13,6 +13,23 @@ rsync:
enabled: true
destination: "xinu.tv:/var/www/static/aoc/"
# Docker container configuration for running cargo aoc
docker:
# Persistent directory for cargo build artifacts (speeds up rebuilds)
# If not specified, uses temporary directory that's cleaned up after each run
build_cache_dir: "/tmp/aocsync/build_cache_dir"
# Persistent directory for cargo registry cache (downloaded dependencies)
# If not specified, uses tmpfs that's cleared after each run
registry_cache_dir: "/tmp/aocsync/registry_cache_dir"
# Container resource limits
memory: "2g" # Memory limit
cpus: "2" # CPU limit
# Docker image to use
image: "rust:latest"
# Repositories to monitor
repositories:
# Example: Single repository with all years