diff --git a/core/morph/config/project.py b/core/morph/config/project.py index f977565..7e6b914 100644 --- a/core/morph/config/project.py +++ b/core/morph/config/project.py @@ -14,7 +14,6 @@ class BuildConfig(BaseModel): - use_custom_dockerfile: bool = False runtime: Optional[str] = None framework: Optional[str] = "morph" package_manager: Optional[str] = None @@ -89,9 +88,115 @@ def save_project(project_root: str, project: MorphProject) -> None: old_config_path = os.path.join(project_root, "morph_project.yaml") if os.path.exists(old_config_path): with open(old_config_path, "w") as f: - yaml.safe_dump(project.model_dump(), f) + f.write(dump_project_yaml(project)) return config_path = os.path.join(project_root, "morph_project.yml") with open(config_path, "w") as f: - yaml.safe_dump(project.model_dump(), f) + f.write(dump_project_yaml(project)) + + +def dump_project_yaml(project: MorphProject) -> str: + source_paths = "\n- ".join([""] + project.source_paths) + + # Default values + build_runtime = "" + build_framework = "" + build_package_manager = "" + build_context = "." + build_args_str = "\n # - ARG_NAME=value\n # - ANOTHER_ARG=value" + deployment_provider = "aws" + deployment_aws_region = "us-east-1" + deployment_aws_memory = "1024" + deployment_aws_timeout = "300" + deployment_aws_concurrency = "1" + deployment_gcp_region = "us-central1" + deployment_gcp_memory = "1Gi" + deployment_gcp_cpu = "1" + deployment_gcp_concurrency = "80" + deployment_gcp_timeout = "300" + + # Set values if build exists + if project.build: + if project.build.runtime: + build_runtime = project.build.runtime or "" + if project.build.framework: + build_framework = project.build.framework or "" + if project.build.package_manager: + build_package_manager = project.build.package_manager or "" + if project.build.context: + build_context = f"{project.build.context}" or "." + if project.build.build_args: + build_args_items = [] + for key, value in project.build.build_args.items(): + build_args_items.append(f"{key}={value}") + build_args_str = ( + "\n # - ".join([""] + build_args_items) + if build_args_items + else "\n # - ARG_NAME=value\n # - ANOTHER_ARG=value" + ) + + # Set values if deployment exists + if project.deployment: + if project.deployment.provider: + deployment_provider = project.deployment.provider or "aws" + if project.deployment.aws: + deployment_aws_region = project.deployment.aws.get("region") or "us-east-1" + deployment_aws_memory = project.deployment.aws.get("memory") or "1024" + deployment_aws_timeout = project.deployment.aws.get("timeout") or "300" + deployment_aws_concurrency = ( + project.deployment.aws.get("concurrency") or "1" + ) + if project.deployment.gcp: + deployment_gcp_region = ( + project.deployment.gcp.get("region") or "us-central1" + ) + deployment_gcp_memory = project.deployment.gcp.get("memory") or "1Gi" + deployment_gcp_cpu = project.deployment.gcp.get("cpu") or "1" + deployment_gcp_concurrency = ( + project.deployment.gcp.get("concurrency") or "80" + ) + deployment_gcp_timeout = project.deployment.gcp.get("timeout") or "300" + else: + # Use default DeploymentConfig + deployment_provider = "aws" + + return f""" +version: '1' + +# Framework Settings +default_connection: {project.default_connection} +source_paths:{source_paths} + +# Cloud Settings +# profile: {project.profile} # Defined in the Profile Section in `~/.morph/credentials` +# project_id: {project.project_id or "null"} + +# Build Settings +build: + # These settings are required when there is no Dockerfile in the project root. + # They define the environment in which the project will be built + runtime: {build_runtime} # python3.9, python3.10, python3.11, python3.12 + framework: {build_framework} + package_manager: {build_package_manager} # pip, poetry, uv + # These settings are required when there is a Dockerfile in the project root. + # They define how the Docker image will be built + # context: {build_context} + # build_args:{build_args_str} + +# Deployment Settings +deployment: + provider: {deployment_provider} # aws or gcp (default is aws) + # These settings are used only when you want to customize the deployment settings + # aws: + # region: {deployment_aws_region} + # memory: {deployment_aws_memory} + # timeout: {deployment_aws_timeout} + # concurrency: {deployment_aws_concurrency} + # gcp: + # region: {deployment_gcp_region} + # memory: {deployment_gcp_memory} + # cpu: {deployment_gcp_cpu} + # concurrency: {deployment_gcp_concurrency} + # timeout: {deployment_gcp_timeout} +""" diff --git a/core/morph/task/deploy.py b/core/morph/task/deploy.py index 629c0e3..3996ff2 100644 --- a/core/morph/task/deploy.py +++ b/core/morph/task/deploy.py @@ -8,14 +8,16 @@ import click import requests +from tqdm import tqdm + from morph.api.cloud.client import MorphApiKeyClientImpl from morph.api.cloud.types import EnvVarObject from morph.cli.flags import Flags from morph.config.project import load_project from morph.task.base import BaseTask from morph.task.utils.file_upload import FileWithProgress +from morph.task.utils.load_dockerfile import get_dockerfile_from_api from morph.task.utils.morph import find_project_root_dir -from tqdm import tqdm class DeployTask(BaseTask): @@ -48,10 +50,31 @@ def __init__(self, args: Flags): self.package_manager = self.project.package_manager # Check Dockerfile existence - self.dockerfile = os.path.join(self.project_root, "Dockerfile") - if not os.path.exists(self.dockerfile): - click.echo(click.style(f"Error: {self.dockerfile} not found", fg="red")) - sys.exit(1) + self.dockerfile_path = os.path.join(self.project_root, "Dockerfile") + self.use_custom_dockerfile = os.path.exists(self.dockerfile_path) + if self.use_custom_dockerfile: + provider = "aws" + if ( + self.project.deployment is not None + and self.project.deployment.provider is not None + ): + provider = self.project.deployment.provider or "aws" + if self.project.build is None: + dockerfile, dockerignore = get_dockerfile_from_api( + "morph", provider, None, None + ) + else: + dockerfile, dockerignore = get_dockerfile_from_api( + self.project.build.framework or "morph", + provider, + self.project.build.package_manager, + self.project.build.runtime, + ) + with open(self.dockerfile_path, "w") as f: + f.write(dockerfile) + dockerignore_path = os.path.join(self.project_root, ".dockerignore") + with open(dockerignore_path, "w") as f: + f.write(dockerignore) # Check Docker availability try: @@ -105,7 +128,7 @@ def run(self): click.echo(click.style("Initiating deployment sequence...", fg="blue")) # 1. Build the source code - self._copy_and_build_source() + self._build_source() # 2. Build the Docker image click.echo(click.style("Building Docker image...", fg="blue")) @@ -387,31 +410,8 @@ def _validate_api_key(self): ) sys.exit(1) - def _copy_and_build_source(self): - click.echo(click.style("Building frontend...", fg="blue")) - try: - # Run npm install and build - subprocess.run( - ["npm", "install"], - cwd=self.project_root, - check=True, - shell=True if sys.platform == "win32" else False, - ) - subprocess.run( - ["npm", "run", "build"], - cwd=self.project_root, - check=True, - shell=True if sys.platform == "win32" else False, - ) - - except subprocess.CalledProcessError as e: - click.echo(click.style(f"Error building frontend: {str(e)}", fg="red")) - sys.exit(1) - except Exception as e: - click.echo(click.style(f"Unexpected error: {str(e)}", fg="red")) - sys.exit(1) - - click.echo(click.style("Building backend...", fg="blue")) + def _build_source(self): + click.echo(click.style("Compiling morph project...", fg="blue")) try: # Compile the morph project subprocess.run( @@ -437,7 +437,7 @@ def _build_docker_image(self) -> str: "-t", self.image_name, "-f", - self.dockerfile, + self.dockerfile_path, self.project_root, ] if self.no_cache: diff --git a/core/morph/task/utils/load_dockerfile.py b/core/morph/task/utils/load_dockerfile.py new file mode 100644 index 0000000..cfbbddb --- /dev/null +++ b/core/morph/task/utils/load_dockerfile.py @@ -0,0 +1,43 @@ +from typing import Any, Dict, Optional, Tuple + +import requests + + +def get_dockerfile_from_api( + framework: str, + provider: str, + package_manager: Optional[str] = None, + runtime: Optional[str] = None, +) -> Tuple[str, str]: + """ + Fetch dockerfile and dockerignore from the Morph API. + + Args: + framework: The framework to get the dockerfile for + provider: The provider to get the dockerfile for + package_manager: Optional package manager to use + runtime: Optional runtime to use + + Returns: + Tuple containing (dockerfile, dockerignore) + """ + url = f"https://dockerfile-template.morph-cb9.workers.dev/dockerfile/{framework}" + + params: Dict[str, Any] = { + "provider": provider, + } + if package_manager: + params["packageManager"] = package_manager + if runtime: + params["runtime"] = runtime + + response = requests.get(url, params=params) + + response.raise_for_status() + + data = response.json() + + if "error" in data: + raise ValueError(data["error"]) + + return data["dockerfile"], data["dockerignore"] diff --git a/pyproject.toml b/pyproject.toml index ccca382..a9a39d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "morph-data" -version = "0.2.1" +version = "0.3.0" description = "Morph is a python-centric full-stack framework for building and deploying data apps." authors = ["Morph "] packages = [