From ccb5d4c82d15cce428cf6b3845452e84f329d3b1 Mon Sep 17 00:00:00 2001 From: DatTT127 Date: Sat, 18 Jul 2026 22:55:49 +0700 Subject: [PATCH] update the gitea --- .../backend_deploy/gitea_modal_build.py | 251 +++++++----------- 1 file changed, 99 insertions(+), 152 deletions(-) diff --git a/workspace/sprint_1_2/CODEBASE/deps/implementation/backend_deploy/gitea_modal_build.py b/workspace/sprint_1_2/CODEBASE/deps/implementation/backend_deploy/gitea_modal_build.py index 901eacb..ee10bad 100644 --- a/workspace/sprint_1_2/CODEBASE/deps/implementation/backend_deploy/gitea_modal_build.py +++ b/workspace/sprint_1_2/CODEBASE/deps/implementation/backend_deploy/gitea_modal_build.py @@ -1,6 +1,9 @@ """ Modal script to build and push VKIST backend Docker image to container registry. +Uses Kaniko (daemonless Docker builder) inside Modal to build and push +without requiring a local Docker daemon. + Run from Gitea runner on Lightsail (2GB RAM) - offloads heavy build to Modal serverless. Usage: @@ -14,8 +17,13 @@ Usage: modal deploy gitea_modal_build.py """ +import base64 +import json import os from pathlib import Path +import subprocess + +import boto3 import modal # ============================================================================= @@ -25,14 +33,17 @@ import modal # Get the directory where this script actually lives on the machine running it SCRIPT_DIR = Path(__file__).resolve().parent -# Walk up until we find a marker directory that identifies the repo root. -# This makes the script robust regardless of where Modal mounts it. -PROJECT_ROOT = SCRIPT_DIR -while not (PROJECT_ROOT / "backend").exists() and PROJECT_ROOT != PROJECT_ROOT.parent: - PROJECT_ROOT = PROJECT_ROOT.parent - -# Explicitly find your local Dockerfile -DOCKERFILE_PATH = SCRIPT_DIR / "Dockerfile" +# When running inside Modal, the project files are baked into /workspace. +# When running locally, walk up from the script location. +_MODAL_WORKSPACE = Path("/workspace") +if (_MODAL_WORKSPACE / "backend").exists(): + PROJECT_ROOT = _MODAL_WORKSPACE + DOCKERFILE_PATH = _MODAL_WORKSPACE / "deps" / "implementation" / "backend_deploy" / "Dockerfile" +else: + PROJECT_ROOT = SCRIPT_DIR + while not (PROJECT_ROOT / "backend").exists() and PROJECT_ROOT != PROJECT_ROOT.parent: + PROJECT_ROOT = PROJECT_ROOT.parent + DOCKERFILE_PATH = SCRIPT_DIR / "Dockerfile" print(f"--- Environment Verification ---") print(f"Script location: {SCRIPT_DIR}") @@ -53,23 +64,30 @@ DEFAULT_TAG = "latest" APP_NAME = "msk-lumina-backend-builder" # ============================================================================= -# Modal Image Definition - uses the project's Dockerfile for consistency +# Modal Image Definition - Kaniko-based builder (no Docker daemon required) # ============================================================================= -# Build the image using the existing multi-stage Dockerfile -# We set context_dir to PROJECT_ROOT so the Dockerfile can see ../../../../backend -backend_image = modal.Image.from_dockerfile( - str(DOCKERFILE_PATH), - context_dir=str(PROJECT_ROOT), -).pip_install( - "docker", # for test_image function - "boto3", # for ECR auth if needed +# Install Kaniko from chainguard-forks and AWS auth dependencies. +# The project source files are baked into the image at /workspace so Kaniko +# can access the build context and Dockerfile at runtime. +builder_image = ( + modal.Image.debian_slim() + .apt_install("curl", "ca-certificates") + .run_commands( + "curl -sSL https://github.com/chainguard-forks/kaniko/releases/latest/download/kaniko -o /usr/local/bin/kaniko", + "chmod +x /usr/local/bin/kaniko", + ) + .pip_install("boto3") # for ECR auth token retrieval + .add_local_dir( + str(PROJECT_ROOT), + remote_path="/workspace", + ) ) -app = modal.App(APP_NAME, image=backend_image) +app = modal.App(APP_NAME, image=builder_image) # ============================================================================= -# Build and Push Function +# Build and Push Function (Kaniko) # ============================================================================= @app.function( @@ -88,138 +106,73 @@ def build_and_push( platform: str = "linux/amd64", ) -> dict: """ - Build the backend Docker image and push to registry. - + Build the backend Docker image with Kaniko and push to registry. + Args: registry: Container registry hostname (e.g., public.ecr.aws/vkist-project) image_name: Image name (e.g., vkist-backend) tag: Image tag (e.g., v1.2.3, latest, git-sha) push: Whether to push to registry platform: Target platform (linux/amd64, linux/arm64) - + Returns: - Dict with image reference, digest, and size info + Dict with image reference and build status """ - import docker - full_image = f"{registry}/{image_name}:{tag}" print(f"Building {full_image} for {platform}") - - # Initialize Docker client (uses Modal's container runtime) - client = docker.from_env() - - # Build using the Dockerfile from project root context - print(f"Build context: {PROJECT_ROOT}") - print(f"Dockerfile: {DOCKERFILE_PATH}") - - # Build the image - image, build_logs = client.images.build( - path=PROJECT_ROOT, - dockerfile=DOCKERFILE_PATH, - tag=full_image, - platform=platform, - rm=True, - pull=True, - buildargs={ - "BUILDKIT_INLINE_CACHE": "1", - }, - ) - - # Stream build logs - for chunk in build_logs: - if "stream" in chunk: - print(chunk["stream"].strip()) - elif "error" in chunk: - raise RuntimeError(f"Build failed: {chunk['error']}") - - print(f"Build complete: {image.id[:12]}") - - # Get image details - image_info = client.images.get(full_image) - size_bytes = image_info.attrs["Size"] - digest = image_info.attrs.get("RepoDigests", ["unknown"])[0] - - result = { - "image": full_image, - "digest": digest, - "size_bytes": size_bytes, - "image_id": image.id, - } - - if push: - print(f"Pushing to {registry}...") - - # Authenticate with registry based on hostname - _authenticate_registry(registry, client) - - # Push image - push_logs = client.images.push( - repository=f"{registry}/{image_name}", - tag=tag, - stream=True, - decode=True, - ) - - for chunk in push_logs: - if "status" in chunk: - print(f" {chunk['status']}: {chunk.get('progress', '')}") - if "error" in chunk: - raise RuntimeError(f"Push failed: {chunk['error']}") - - print(f"Push complete: {full_image}") - - # Get digest after push - pushed_image = client.images.get(full_image) - result["digest"] = pushed_image.attrs.get("RepoDigests", ["unknown"])[0] - - return result - -def _authenticate_registry(registry: str, client): - """Authenticate Docker client with the target registry.""" - import subprocess - - if "ecr.aws" in registry or "ecr." in registry: - # AWS ECR (public or private) + # Prepare docker config for Kaniko if pushing to ECR + docker_config_dir = None + if push and ("ecr.aws" in registry or "ecr." in registry): + print(f"Configuring ECR authentication...") region = os.getenv("AWS_REGION", "us-east-1") - print(f"Authenticating with AWS ECR ({region})...") - subprocess.run([ - "aws", "ecr", "get-login-password", "--region", region - ], check=True, capture_output=True) - # Note: Modal's aws-ecr-credentials secret should handle this via env vars - -@app.function( - image=backend_image, - timeout=120, - secrets=[modal.Secret.from_name("aws-secrets")], -) -def test_image(registry: str, image_name: str, tag: str) -> dict: - """Test the built image by running a quick health check.""" - import docker - - full_image = f"{registry}/{image_name}:{tag}" - client = docker.from_env() - - print(f"Pulling {full_image} for testing...") - client.images.pull(full_image) - - print("Running basic smoke test...") - output = client.containers.run( - full_image, - command=[ - "python", "-c", - "import sys; print('Python', sys.version); " - "import cv2; print('OpenCV', cv2.__version__); " - ], - remove=True, - detach=False, - ) + ecr = boto3.client("ecr", region_name=region) + auth = ecr.get_authorization_token() + token = auth["authorizationData"][0]["authorizationToken"] + username, password = base64.b64decode(token).decode().split(":") + + config = { + "auths": { + registry: { + "username": username, + "password": password, + } + } + } + + docker_config_dir = PROJECT_ROOT / ".kaniko" + docker_config_dir.mkdir(exist_ok=True) + (docker_config_dir / "config.json").write_text(json.dumps(config)) + print(f"ECR auth configured for {registry}") + + # Build with Kaniko + kaniko_cmd = [ + "/usr/local/bin/kaniko", + "--context", str(PROJECT_ROOT), + "--dockerfile", str(DOCKERFILE_PATH), + "--destination", full_image, + "--platform", platform, + "--verbosity", "info", + ] + + if docker_config_dir: + kaniko_cmd.extend(["--docker-config", str(docker_config_dir)]) + + print(f"Running Kaniko: {' '.join(kaniko_cmd)}") + result = subprocess.run(kaniko_cmd, capture_output=True, text=True) + + print(result.stdout) + if result.returncode != 0: + raise RuntimeError(f"Kaniko build failed:\n{result.stderr}") + + print(f"Push complete: {full_image}") return { - "status": "healthy", - "output": output.decode().strip(), "image": full_image, + "digest": "unknown", + "size_bytes": 0, + "image_id": "kaniko-build", } @@ -237,18 +190,21 @@ def main( test: bool = False, ): """ - Build and push backend Docker image via Modal. - + Build and push backend Docker image via Kaniko on Modal. + Args: registry: Container registry (default: public.ecr.aws/vkist-project) image_name: Image name (default: vkist-backend) tag: Image tag (default: latest) push: Push to registry (default: true) platform: Target platform (default: linux/amd64) - test: Run health check after build (default: false) + test: Run health check after build (default: false, unsupported with Kaniko) """ + if test: + print("WARNING: post-push testing is not supported with Kaniko. Skipping test.") + print("=" * 60) - print("MSK-Lumina Backend Docker Build on Modal") + print("MSK-Lumina Backend Docker Build on Modal (Kaniko)") print("=" * 60) print(f"Registry: {registry}") print(f"Image: {image_name}") @@ -257,8 +213,7 @@ def main( print(f"Platform: {platform}") print(f"Test: {test}") print("=" * 60) - - # Build and push + result = build_and_push.remote( registry=registry, image_name=image_name, @@ -266,26 +221,18 @@ def main( push=push, platform=platform, ) - + print("\n" + "=" * 60) print("BUILD COMPLETE") print("=" * 60) print(f"Image: {result['image']}") print(f"Digest: {result['digest']}") - print(f"Size: {result['size_bytes'] / 1024 / 1024:.1f} MB") print("=" * 60) - + # Output for CI/CD parsing (GitHub Actions / Gitea Actions format) print(f"::set-output name=image::{result['image']}") print(f"::set-output name=digest::{result['digest']}") print(f"::set-output name=size_mb::{result['size_bytes'] / 1024 / 1024:.1f}") - - # Optional test - if test and push: - print("\nRunning health check...") - test_result = test_image.remote(registry, image_name, tag) - print(f"Health check: {test_result['status']}") - print(f"Output: {test_result['output']}") if __name__ == "__main__": @@ -299,4 +246,4 @@ if __name__ == "__main__": print(" modal deploy gitea_modal_build.py") else: print("Use: modal run gitea_modal_build.py [options]") - print("Or: modal deploy gitea_modal_build.py") \ No newline at end of file + print("Or: modal deploy gitea_modal_build.py")