Skip to content

Commit

Permalink
Add CLI for making images iluvatar compatible
Browse files Browse the repository at this point in the history
  • Loading branch information
Bhatt21 committed Feb 11, 2025
1 parent 041eb53 commit 43c5307
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 0 deletions.
Empty file.
68 changes: 68 additions & 0 deletions src/cli/iluvatar_cli/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import click
import os
from iluvatar_cli.runtime_handler.python.handler import PythonRuntimeHandler

@click.command()
@click.option("--function-dir", required=True, help="Path to the function directory containing your code and dependencies.")
@click.option("--runtime", default="python", help="Runtime to use (default: python).")
@click.option("--tag", default="iluvatar-runtime:latest", help="Docker image tag (default: iluvatar-runtime:latest).")
@click.option("--docker-user", default=None, help="Docker registry username.")
@click.option("--docker-pass", default=None, help="Docker registry password.")
@click.option("--docker-registry", default="docker.io", help="Docker registry URL (default: docker.io).")
def main(function_dir, runtime, tag, docker_user, docker_pass, docker_registry):
"""
CLI tool to build a Docker runtime for FaaS.
This tool validates that the entry point file is Iluvatar-compatible (i.e. it defines a main() function
that returns a dict), ensures that the dependencies file exists (or creates an empty one if missing),
builds a Docker image based on the specified runtime, and optionally pushes it to a Docker registry.
"""
function_dir = os.path.abspath(function_dir)
click.echo(f"Using function directory: {function_dir}")
click.echo(f"Runtime: {runtime}")
click.echo(f"Docker image tag: {tag}")

if runtime.lower() == "python":
handler = PythonRuntimeHandler()
# Future handlers for Node.js, Go, etc. can be added here.
else:
click.echo(f"Unsupported runtime: {runtime}")
return

try:
click.echo("Validating function directory...")
handler.validate_function_directory(function_dir)
except Exception as e:
click.echo(f"Validation failed: {e}")
return

try:
click.echo("Ensuring dependencies file exists...")
handler.ensure_dependencies(function_dir)
except Exception as e:
click.echo(f"Error handling dependencies: {e}")
return

try:
click.echo("Building Docker image...")
handler.build_image(function_dir, tag)
except Exception as e:
click.echo(f"Error building Docker image: {e}")
return

click.echo("Docker image built successfully.")

# If Docker registry credentials are provided, push the image.
if docker_user and docker_pass:
click.echo("Pushing Docker image to registry...")
try:
handler.push_docker_image(tag, docker_user, docker_pass, docker_registry)
except Exception as e:
click.echo(f"Error pushing Docker image: {e}")
return
click.echo("Docker image pushed successfully.")
else:
click.echo("No Docker registry credentials provided. Skipping push.")

if __name__ == "__main__":
main()
74 changes: 74 additions & 0 deletions src/cli/iluvatar_cli/docker_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import os
import json
import shutil
import subprocess
import tempfile

def build_docker_image_from_directory(function_dir: str, tag: str,
base_image: str, install_command: str, server_command: str) -> None:
"""
Build a Docker image using the contents of the function directory.
The function directory is copied into a temporary build context,
a Dockerfile is written that:
- Uses the specified base image.
- Copies all files from the function directory.
- Runs the given install command (e.g., to install dependencies).
- Sets the server command as the container's CMD.
"""
with tempfile.TemporaryDirectory() as tmpdir:
app_dir = os.path.join(tmpdir, "app")
shutil.copytree(function_dir, app_dir, dirs_exist_ok=True)

# cmd_json = json.dumps(server_command.split())
dockerfile_content = f"""
FROM {base_image}
WORKDIR /app
COPY app/ .
RUN {install_command}
ENTRYPOINT ["gunicorn", "-w", "1", "server:app"]
""".strip()

dockerfile_path = os.path.join(tmpdir, "Dockerfile")
with open(dockerfile_path, "w") as f:
f.write(dockerfile_content)

command = ["docker", "build", "-t", tag, tmpdir]
result = subprocess.run(command, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"Docker build failed: {result.stderr}")


def push_docker_image(tag: str, docker_username: str, docker_password: str, docker_registry: str) -> None:
"""
Push a Docker image to a registry.
Parameters:
tag (str): The local Docker image tag.
docker_username (str): Docker registry username.
docker_password (str): Docker registry password.
docker_registry (str): Docker registry URL (e.g., "docker.io").
"""
# Log in to the Docker registry.
login_command = [
"docker", "login", docker_registry,
"-u", docker_username,
"-p", docker_password
]
login_result = subprocess.run(login_command, capture_output=True, text=True)
if login_result.returncode != 0:
raise RuntimeError(f"Docker login failed: {login_result.stderr}")
# If the tag does not include a slash (registry prefix), tag it with the registry.
if "/" in tag:
full_tag = tag
else:
full_tag = f"{docker_registry}/{tag}"
tag_command = ["docker", "tag", tag, full_tag]
tag_result = subprocess.run(tag_command, capture_output=True, text=True)
if tag_result.returncode != 0:
raise RuntimeError(f"Docker tag failed: {tag_result.stderr}")

push_command = ["docker", "push", full_tag]
push_result = subprocess.run(push_command, capture_output=True, text=True)
if push_result.returncode != 0:
raise RuntimeError(f"Docker push failed: {push_result.stderr}")
Empty file.
33 changes: 33 additions & 0 deletions src/cli/iluvatar_cli/runtime_handler/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from abc import ABC, abstractmethod

class BaseRuntimeHandler(ABC):
@abstractmethod
def validate_function_directory(self, function_dir: str) -> None:
"""
Validate that the function directory is compatible with the runtime.
For Python, for example, this means the directory must contain a main.py that defines
a main() function with no parameters and returns a dict.
"""
pass

@abstractmethod
def ensure_dependencies(self, function_dir: str) -> None:
"""
Ensure that the dependencies file exists in the function directory,
or create a default one if needed.
"""
pass

@abstractmethod
def build_image(self, function_dir: str, tag: str) -> None:
"""
Build a Docker image from the function directory.
"""
pass

@abstractmethod
def push_docker_image(self, tag: str, docker_username: str, docker_password: str, docker_registry: str) -> None:
"""
Push a Docker image to a registry.
"""
pass
Empty file.
3 changes: 3 additions & 0 deletions src/cli/iluvatar_cli/runtime_handler/python/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Python runtime configuration.
BASE_IMAGE = "docker.io/sakbhatt/iluvatar_base_image"
SERVER_COMMAND = "gunicorn -w 1 server:app"
34 changes: 34 additions & 0 deletions src/cli/iluvatar_cli/runtime_handler/python/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import os
from iluvatar_cli.runtime_handler.base import BaseRuntimeHandler
from iluvatar_cli.runtime_handler.python import validator
from iluvatar_cli import docker_builder
from .config import BASE_IMAGE, SERVER_COMMAND

class PythonRuntimeHandler(BaseRuntimeHandler):
def validate_function_directory(self, function_dir: str) -> None:
validator.validate_function_directory(function_dir)

def ensure_dependencies(self, function_dir: str) -> None:
"""
Ensure that the function directory contains a requirements.txt file.
If not, create an empty one.
"""
req_path = os.path.join(function_dir, "requirements.txt")
if not os.path.exists(req_path):
with open(req_path, "w") as f:
f.write("")

def build_image(self, function_dir: str, tag: str) -> None:
install_command = "pip install --no-cache-dir -r requirements.txt"
docker_builder.build_docker_image_from_directory(
function_dir,
tag,
base_image=BASE_IMAGE,
install_command=install_command,
server_command=SERVER_COMMAND
)
def push_docker_image(self, tag: str, docker_username: str, docker_password: str, docker_registry: str) -> None:
"""
Push the Docker image to the registry.
"""
docker_builder.push_docker_image(tag, docker_username, docker_password, docker_registry)
29 changes: 29 additions & 0 deletions src/cli/iluvatar_cli/runtime_handler/python/validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import os
import importlib.util
import inspect

def validate_function_directory(function_dir: str) -> None:
"""
Validate that the function directory:
- Exists and is a directory.
- Contains a main.py file.
"""
if not os.path.isdir(function_dir):
raise NotADirectoryError(f"{function_dir} is not a valid directory.")

entry_point = os.path.join(function_dir, "main.py")
if not os.path.exists(entry_point):
raise FileNotFoundError("main.py not found in the function directory.")

# Load the module from main.py.
module_name = "main"
spec = importlib.util.spec_from_file_location(module_name, entry_point)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)

if not hasattr(module, "main"):
raise ValueError("The entry point (main.py) does not define a main() function.")

main_func = getattr(module, "main")
if not callable(main_func):
raise ValueError("main is not callable.")
15 changes: 15 additions & 0 deletions src/cli/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from setuptools import setup, find_packages

setup(
name="iluvatar_cli",
version="1.0.0",
packages=find_packages(),
install_requires=[
"click",
],
entry_points={
"console_scripts": [
"iluvatar_cli=iluvatar_cli.cli:main",
],
},
)

0 comments on commit 43c5307

Please sign in to comment.