""" API routes for Docker file generation. """ import os import uuid from fastapi import APIRouter, Depends, HTTPException, Request, status from app.api.deps import get_logger, get_settings from app.core.security import limiter from app.models.schemas import ( DockerfileConfig, DockerComposeConfig, DockerGeneratedFiles, ) from app.services.docker_generator import DockerGenerator from app.services.dockerfile_validator import DockerfileValidator from app.core.config import settings from app.core.logging import logger import jinja2 router = APIRouter() # Create Docker Generator instance template_env = jinja2.Environment( loader=jinja2.FileSystemLoader(settings.templates_dir), trim_blocks=True, lstrip_blocks=True, autoescape=True, cache_size=0, ) docker_generator = DockerGenerator(template_env, settings, logger) dockerfile_validator = DockerfileValidator(logger) @router.post("/generate-dockerfile", response_model=DockerGeneratedFiles) @limiter.limit("10/minute") async def generate_dockerfile( request: Request, config: DockerfileConfig, settings=Depends(get_settings), logger=Depends(get_logger), ): """Generate a Dockerfile based on stack type.""" client_ip = request.client.host if request.client else "unknown" logger.info(f"Dockerfile generation request from {client_ip} for {config.stackType}") try: # Generate unique filename file_id = str(uuid.uuid4())[:8] # Generate Dockerfile dockerfile_content = docker_generator.generate_dockerfile(config, file_id) dockerfile_filename = f"Dockerfile-{file_id}" dockerfile_path = settings.generated_dir / dockerfile_filename # Ensure generated directory exists dockerfile_path.parent.mkdir(exist_ok=True) # Write Dockerfile with open(dockerfile_path, "w") as f: f.write(dockerfile_content) # Generate .dockerignore dockerignore_content = docker_generator.generate_dockerignore(config, file_id) dockerignore_filename = f".dockerignore-{file_id}" dockerignore_path = settings.generated_dir / dockerignore_filename with open(dockerignore_path, "w") as f: f.write(dockerignore_content) # Create URLs dockerfile_url = f"/download/{dockerfile_filename}" dockerignore_url = f"/download/{dockerignore_filename}" response = DockerGeneratedFiles( dockerfile_url=dockerfile_url, dockerfile_path=str(dockerfile_path), dockerignore_url=dockerignore_url, dockerignore_path=str(dockerignore_path), description=f"Dockerfile for {config.stackType} " f"({'multi-stage' if config.multiStage else 'single-stage'}, " f"{'development' if config.developmentMode else 'production'})", ) return response except Exception as e: logger.error(f"Failed to generate Dockerfile: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to generate Dockerfile: {str(e)}", ) @router.post("/generate-docker-compose", response_model=DockerGeneratedFiles) @limiter.limit("10/minute") async def generate_docker_compose( request: Request, config: DockerComposeConfig, settings=Depends(get_settings), logger=Depends(get_logger), ): """Generate docker-compose.yml and supporting files.""" client_ip = request.client.host if request.client else "unknown" logger.info( f"Docker Compose generation request from {client_ip} for {config.stackType}" ) try: # Generate unique filename file_id = str(uuid.uuid4())[:8] # Generate docker-compose.yml compose_content = docker_generator.generate_docker_compose(config, file_id) compose_filename = f"docker-compose-{file_id}.yml" compose_path = settings.generated_dir / compose_filename # Ensure generated directory exists compose_path.parent.mkdir(exist_ok=True) # Write compose file with open(compose_path, "w") as f: f.write(compose_content) response = DockerGeneratedFiles( compose_url=f"/download/{compose_filename}", compose_path=str(compose_path), description=f"Docker Compose for {config.stackType} ({config.mode} mode)", ) # Generate .dockerignore dockerignore_content = docker_generator.generate_dockerignore(config, file_id) dockerignore_filename = f".dockerignore-{file_id}" dockerignore_path = settings.generated_dir / dockerignore_filename with open(dockerignore_path, "w") as f: f.write(dockerignore_content) response.dockerignore_url = f"/download/{dockerignore_filename}" response.dockerignore_path = str(dockerignore_path) # Generate .env.example if requested if config.includeEnvFile: env_content = docker_generator.generate_env_example(config, file_id) env_filename = f".env.example-{file_id}" env_path = settings.generated_dir / env_filename with open(env_path, "w") as f: f.write(env_content) response.env_url = f"/download/{env_filename}" response.env_path = str(env_path) # Generate build script build_script_content = docker_generator.generate_build_script(config, file_id) build_script_filename = f"docker-build-{file_id}.sh" build_script_path = settings.generated_dir / build_script_filename with open(build_script_path, "w") as f: f.write(build_script_content) # Make script executable os.chmod(build_script_path, 0o755) response.build_script_url = f"/download/{build_script_filename}" response.build_script_path = str(build_script_path) return response except Exception as e: logger.error(f"Failed to generate docker-compose: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to generate docker-compose: {str(e)}", ) @router.get("/validate-dockerfile/{file_id}") @limiter.limit("20/minute") async def validate_dockerfile( request: Request, file_id: str, logger=Depends(get_logger), settings=Depends(get_settings), ): """ Validate a generated Dockerfile for syntax and best practices """ client_ip = request.client.host if request.client else "unknown" logger.info(f"Dockerfile validation request from {client_ip} for file {file_id}") try: # Find the Dockerfile dockerfile_path = settings.generated_dir / f"Dockerfile-{file_id}" if not dockerfile_path.exists(): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Dockerfile not found: {file_id}" ) # Read Dockerfile content with open(dockerfile_path, 'r') as f: dockerfile_content = f.read() # Validate validation_result = dockerfile_validator.validate_dockerfile( dockerfile_content, stack_type="unknown" # We could extract this from config if needed ) logger.info(f"Validation result for {file_id}: valid={validation_result['valid']}, score={validation_result['score']}") return validation_result except HTTPException: raise except Exception as e: logger.error(f"Failed to validate Dockerfile: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to validate Dockerfile: {str(e)}" ) @router.get("/validate-docker-compose/{file_id}") @limiter.limit("20/minute") async def validate_docker_compose( request: Request, file_id: str, logger=Depends(get_logger), settings=Depends(get_settings), ): """ Validate a generated docker-compose.yml for syntax and best practices """ client_ip = request.client.host if request.client else "unknown" logger.info(f"Docker Compose validation request from {client_ip} for file {file_id}") try: # Find the docker-compose file compose_path = settings.generated_dir / f"docker-compose-{file_id}.yml" if not compose_path.exists(): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Docker Compose file not found: {file_id}" ) # Read compose content with open(compose_path, 'r') as f: compose_content = f.read() # Validate validation_result = dockerfile_validator.validate_docker_compose(compose_content) logger.info(f"Validation result for {file_id}: valid={validation_result['valid']}, score={validation_result['score']}") return validation_result except HTTPException: raise except Exception as e: logger.error(f"Failed to validate docker-compose: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to validate docker-compose: {str(e)}" )