Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions projects/fal/src/fal/api/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ class DeploymentResult:
auth_mode: str


@dataclass
class BuildResult:
revision: str
app_name: str | None


@dataclass
class PreparedDeployment:
host: FalServerlessHost
Expand Down Expand Up @@ -412,6 +418,110 @@ def _execute_loaded_deployment(
)


def _execute_loaded_build(
*,
host: FalServerlessHost,
loaded: LoadedFunction,
app_data: AppData,
environment_name: str | None = None,
result_handler: ResultHandler | None = None,
build_result_handler: ResultHandler | None = None,
prepare_options_handler: ProgressCallback | None = None,
) -> BuildResult:
from fal.api import FalServerlessError

build_result_handler = (
result_handler if build_result_handler is None else build_result_handler
)

isolated_function = replace(
loaded.function,
options=host.prepare_options(
loaded.function.options,
func=loaded.function.func,
on_progress=prepare_options_handler,
),
)

host.build_environment(
isolated_function.options,
application_name=loaded.app_name,
environment_name=environment_name,
result_handler=build_result_handler,
)

isolated_function.fetch_metadata(build_environment=False)

metadata = dict(isolated_function.build_metadata())
if app_data.message is not None:
metadata["message"] = app_data.message
if app_data.annotations:
metadata["annotations"] = dict(app_data.annotations)

# Registering without an application_name stores the revision and leaves
# every alias pointing where it already pointed.
result = host.register(
func=isolated_function.func,
options=isolated_function.options,
application_name=None,
source_code=loaded.source_code,
metadata=metadata,
deployment_strategy=app_data.deployment_strategy or "rolling",
scale=app_data.reset_scale,
environment_name=environment_name,
result_handler=result_handler,
entrypoint=isolated_function.run_entrypoint,
build_environment=False,
)

if not result or not result.result:
raise FalServerlessError(
"Build failed: The server did not confirm the revision. "
"This may indicate a network issue or server error. "
"Please try again."
)

return BuildResult(
revision=result.result.application_id,
app_name=loaded.app_name,
)


def build(
client: SyncServerlessClient,
app_ref: str | tuple[str | None, str | None] | None = None,
*,
app_name: str | None = None,
force_env_build: bool = False,
environment_name: str | None = None,
message: str | None = None,
annotations: dict[str, str] | None = None,
result_handler: ResultHandler | None = None,
build_result_handler: ResultHandler | None = None,
) -> BuildResult:
resolved_app_ref, app_data = _resolve_deployment_reference(
app_ref,
app_name=app_name,
message=message,
annotations=annotations,
)
prepared = _prepare_deployment_from_reference(
client,
resolved_app_ref,
app_data,
force_env_build=force_env_build,
environment_name=environment_name,
)
return _execute_loaded_build(
host=prepared.host,
loaded=prepared.loaded,
app_data=prepared.app_data,
environment_name=prepared.environment_name,
result_handler=result_handler,
build_result_handler=build_result_handler,
)


def prepare_deployment(
client: SyncServerlessClient,
app_ref: str | tuple[str | None, str | None] | None = None,
Expand Down
111 changes: 111 additions & 0 deletions projects/fal/src/fal/cli/build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from __future__ import annotations

import json

from fal.api.client import SyncServerlessClient

from .deploy import _resolve_team_and_app_ref
from .parser import FalClientParser, RefAction, add_env_argument, get_output_parser


def _build(args):
from fal.api.deploy import build as build_api

from ._result_handlers import (
CliBuildEnvironmentResultHandler,
CliRegisterResultHandler,
)

team, app_ref = _resolve_team_and_app_ref(args)

client = SyncServerlessClient(host=args.host, team=team)
res = build_api(
client,
app_ref,
app_name=args.app_name,
force_env_build=args.no_cache,
environment_name=args.env,
result_handler=CliRegisterResultHandler(console=args.console),
build_result_handler=CliBuildEnvironmentResultHandler(console=args.console),
)

_render_build_result(args, res)


def _render_build_result(args, res) -> None:
if args.output == "json":
args.console.print(
json.dumps({"revision": res.revision, "app_name": res.app_name})
)
elif args.output == "pretty":
from fal.console.icons import get_check_icon

args.console.print(
f"{get_check_icon(args.console)} Built successfully",
style="bold green",
)
args.console.print("")
args.console.print(f"Revision: {res.revision}")
args.console.print("")
args.console.print(
"[dim]This revision is not serving traffic. "
"Deploy it with `fal deploy` to point an alias at it.[/dim]"
)
else:
raise AssertionError(f"Invalid output format: {args.output}")


def add_parser(main_subparsers, parents):
build_help = (
"Build a fal application into a new revision without deploying it. "
"No alias is pointed at the revision, so it does not serve traffic."
)

epilog = (
"Examples:\n"
" fal build\n"
" fal build path/to/myfile.py\n"
" fal build path/to/myfile.py::MyApp\n"
" fal build my-app\n"
)

parser = main_subparsers.add_parser(
"build",
parents=[
*parents,
get_output_parser(),
FalClientParser(add_help=False),
],
description=build_help,
help=build_help,
epilog=epilog,
)

parser.add_argument(
"app_ref",
nargs="?",
action=RefAction,
help=(
"Application reference. Either a file path or a file path and a "
"function name separated by '::'. If no reference is provided, the "
"command will look for a pyproject.toml file with a [tool.fal.apps] "
"section and build the application specified with the provided app name.\n"
"File path example: path/to/myfile.py::MyApp\n"
"App name example: my-app (configure team in pyproject.toml)\n"
),
)

parser.add_argument(
"--app-name",
help="Application name to build with.",
)

parser.add_argument(
"--no-cache",
action="store_true",
help="Do not use the cache for the environment build.",
)

add_env_argument(parser)

parser.set_defaults(func=_build)
2 changes: 2 additions & 0 deletions projects/fal/src/fal/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
api,
apps,
auth,
build,
completion,
create,
deploy,
Expand Down Expand Up @@ -57,6 +58,7 @@ def _get_main_parser() -> argparse.ArgumentParser:
apps,
environments,
queue,
build,
deploy,
run,
keys,
Expand Down
Loading