forked from COS-IN/iluvatar-faas
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add CLI for making images iluvatar compatible
- Loading branch information
Showing
10 changed files
with
256 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
], | ||
}, | ||
) |