update the gitea
This commit is contained in:
@@ -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,13 +33,16 @@ 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.
|
||||
# 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
|
||||
|
||||
# Explicitly find your local Dockerfile
|
||||
DOCKERFILE_PATH = SCRIPT_DIR / "Dockerfile"
|
||||
|
||||
print(f"--- Environment Verification ---")
|
||||
@@ -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,7 +106,7 @@ 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)
|
||||
@@ -98,128 +116,63 @@ def build_and_push(
|
||||
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()
|
||||
# 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")
|
||||
|
||||
# Build using the Dockerfile from project root context
|
||||
print(f"Build context: {PROJECT_ROOT}")
|
||||
print(f"Dockerfile: {DOCKERFILE_PATH}")
|
||||
ecr = boto3.client("ecr", region_name=region)
|
||||
auth = ecr.get_authorization_token()
|
||||
token = auth["authorizationData"][0]["authorizationToken"]
|
||||
username, password = base64.b64decode(token).decode().split(":")
|
||||
|
||||
# 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,
|
||||
config = {
|
||||
"auths": {
|
||||
registry: {
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if push:
|
||||
print(f"Pushing to {registry}...")
|
||||
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}")
|
||||
|
||||
# Authenticate with registry based on hostname
|
||||
_authenticate_registry(registry, client)
|
||||
# Build with Kaniko
|
||||
kaniko_cmd = [
|
||||
"/usr/local/bin/kaniko",
|
||||
"--context", str(PROJECT_ROOT),
|
||||
"--dockerfile", str(DOCKERFILE_PATH),
|
||||
"--destination", full_image,
|
||||
"--platform", platform,
|
||||
"--verbosity", "info",
|
||||
]
|
||||
|
||||
# Push image
|
||||
push_logs = client.images.push(
|
||||
repository=f"{registry}/{image_name}",
|
||||
tag=tag,
|
||||
stream=True,
|
||||
decode=True,
|
||||
)
|
||||
if docker_config_dir:
|
||||
kaniko_cmd.extend(["--docker-config", str(docker_config_dir)])
|
||||
|
||||
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"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}")
|
||||
|
||||
# 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)
|
||||
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,
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "healthy",
|
||||
"output": output.decode().strip(),
|
||||
"image": full_image,
|
||||
"digest": "unknown",
|
||||
"size_bytes": 0,
|
||||
"image_id": "kaniko-build",
|
||||
}
|
||||
|
||||
|
||||
@@ -237,7 +190,7 @@ 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)
|
||||
@@ -245,10 +198,13 @@ def main(
|
||||
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}")
|
||||
@@ -258,7 +214,6 @@ def main(
|
||||
print(f"Test: {test}")
|
||||
print("=" * 60)
|
||||
|
||||
# Build and push
|
||||
result = build_and_push.remote(
|
||||
registry=registry,
|
||||
image_name=image_name,
|
||||
@@ -272,7 +227,6 @@ def main(
|
||||
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)
|
||||
@@ -280,13 +234,6 @@ def main(
|
||||
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__":
|
||||
# Allow direct execution for debugging
|
||||
|
||||
Reference in New Issue
Block a user