Skip to content

Commit 50afb81

Browse files
authored
feat: add build --push command (#1485)
1 parent e621f24 commit 50afb81

File tree

8 files changed

+217
-131
lines changed

8 files changed

+217
-131
lines changed

deploy/sdk/src/dynamo/sdk/cli/build.py

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,15 @@
3838
from rich.progress import Progress, SpinnerColumn, TextColumn
3939

4040
from dynamo.sdk import DYNAMO_IMAGE
41+
from dynamo.sdk.core.protocol.deployment import Service
4142
from dynamo.sdk.core.protocol.interface import (
43+
DynamoConfig,
4244
DynamoTransport,
4345
LinkedServices,
4446
ServiceInterface,
4547
)
4648
from dynamo.sdk.core.runner import TargetEnum
49+
from dynamo.sdk.lib.utils import upload_graph
4750

4851
logger = logging.getLogger(__name__)
4952
console = Console()
@@ -104,7 +107,7 @@ class ServiceConfig(BaseModel):
104107
resources: t.Dict[str, t.Any] = Field(default_factory=dict)
105108
workers: t.Optional[int] = None
106109
image: str = "dynamo:latest"
107-
dynamo: t.Dict[str, t.Any] = Field(default_factory=dict)
110+
dynamo: DynamoConfig = Field(default_factory=DynamoConfig)
108111
http_exposed: bool = False
109112
api_endpoints: t.List[str] = Field(default_factory=list)
110113

@@ -141,7 +144,7 @@ def from_service(cls, service: ServiceInterface[T]) -> ServiceInfo:
141144
resources=service.config.resources.model_dump(),
142145
workers=service.config.workers,
143146
image=image,
144-
dynamo=service.config.dynamo.model_dump(),
147+
dynamo=DynamoConfig(**service.config.dynamo.model_dump()),
145148
http_exposed=len(api_endpoints) > 0,
146149
api_endpoints=api_endpoints,
147150
)
@@ -155,7 +158,7 @@ def from_service(cls, service: ServiceInterface[T]) -> ServiceInfo:
155158

156159

157160
class BuildConfig(BaseModel):
158-
"""Configuration for building a Dynamo pipeline."""
161+
"""Configuration for building a Dynamo graph."""
159162

160163
service: str
161164
name: t.Optional[str] = None
@@ -277,7 +280,7 @@ def dynamo_service(
277280
cls,
278281
build_config: BuildConfig,
279282
build_ctx: t.Optional[str] = None,
280-
) -> t.Any:
283+
) -> ServiceInterface:
281284
"""Get a dynamo service from config."""
282285
build_ctx = (
283286
os.getcwd()
@@ -367,6 +370,26 @@ def generate_manifests(self) -> None:
367370
with open(os.path.join(self.path, "dynamo.yaml"), "w") as f:
368371
yaml.dump(manifest_dict, f, default_flow_style=False)
369372

373+
def get_entry_service(self) -> Service:
374+
"""Get the entry service."""
375+
for service in self.info.services:
376+
if service.name == self.info.entry_service:
377+
entry_service = service
378+
break
379+
else:
380+
raise ValueError(
381+
f"Entry service {self.info.entry_service} not found in services"
382+
)
383+
384+
return Service(
385+
service_name=self.info.service,
386+
name=self.info.entry_service,
387+
namespace=entry_service.config.dynamo.namespace,
388+
version=self.info.tag.version,
389+
path=self.path,
390+
envs=self.info.envs,
391+
)
392+
370393
@staticmethod
371394
def load_service(service_path: str, working_dir: str) -> t.Any:
372395
"""Load a service from a path."""
@@ -481,6 +504,9 @@ def build(
481504
service: str = typer.Argument(
482505
..., help="Service specification in the format module:ServiceClass"
483506
),
507+
endpoint: t.Optional[str] = typer.Option(
508+
None, "--endpoint", "-e", help="Dynamo Cloud endpoint", envvar="DYNAMO_CLOUD"
509+
),
484510
output_dir: t.Optional[str] = typer.Option(
485511
None, "--output-dir", "-o", help="Output directory for the build"
486512
),
@@ -490,13 +516,25 @@ def build(
490516
containerize: bool = typer.Option(
491517
False,
492518
"--containerize",
493-
help="Containerize the dynamo pipeline after building.",
519+
help="Containerize the dynamo graph after building.",
520+
),
521+
push: bool = typer.Option(
522+
False,
523+
"--push",
524+
help="Push the built dynamo graph to the Dynamo cloud remote API store.",
494525
),
495526
) -> None:
496-
"""Packages Dynamo service for deployment. Optionally builds a docker container."""
527+
"""Packages Dynamo service for deployment. Optionally builds and/or pushes a docker container."""
497528
from dynamo.sdk.cli.utils import configure_target_environment
498529

499530
configure_target_environment(TargetEnum.DYNAMO)
531+
if push:
532+
containerize = True
533+
if endpoint is None:
534+
console.print(
535+
"[bold red]Error: --push requires --endpoint, -e, or DYNAMO_CLOUD environment variable to be set.[/]"
536+
)
537+
raise typer.Exit(1)
500538

501539
# Determine output directory
502540
if output_dir is None:
@@ -558,9 +596,12 @@ def build(
558596
next_steps = []
559597
if not containerize:
560598
next_steps.append(
561-
"\n\n* Containerize your Dynamo pipeline with "
599+
"\n\n* Containerize your Dynamo graph with "
562600
"`dynamo build --containerize <service_name>`:\n"
563601
f" $ dynamo build --containerize {service}"
602+
"\n\n* Push your Dynamo graph to the Dynamo cloud with "
603+
"`dynamo build --push <service_name>`:\n"
604+
f" $ dynamo build --push {service}"
564605
)
565606

566607
if next_steps:
@@ -597,13 +638,29 @@ def build(
597638
check=True,
598639
)
599640
console.print(f"[green]Successfully built Docker image {image_name}.")
641+
642+
if push:
643+
# Upload the graph to the Dynamo cloud remote API store
644+
with Progress(
645+
SpinnerColumn(),
646+
TextColumn(
647+
f"[bold green]Pushing graph {image_name} to Dynamo cloud..."
648+
),
649+
transient=True,
650+
) as progress:
651+
progress.add_task("push", total=None)
652+
entry_service = package.get_entry_service()
653+
upload_graph(endpoint, image_name, entry_service)
654+
console.print(
655+
f"[green]Successfully pushed graph {image_name} to Dynamo cloud."
656+
)
600657
except Exception as e:
601-
console.print(f"[red]Error building package: {str(e)}")
658+
console.print(f"[red]Error with build: {str(e)}")
602659
raise
603660

604661

605662
def generate_random_tag() -> str:
606-
"""Generate a random tag for the Dynamo pipeline."""
663+
"""Generate a random tag for the Dynamo graph."""
607664
return f"{uuid.uuid4().hex[:8]}"
608665

609666

deploy/sdk/src/dynamo/sdk/cli/deployment.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def _handle_deploy_create(
146146
# TODO: hardcoding this is a hack to get the services for the deployment
147147
# we should find a better way to do this once build is finished/generic
148148
configure_target_environment(TargetEnum.DYNAMO)
149-
entry_service = load_entry_service(config.pipeline)
149+
entry_service = load_entry_service(config.graph)
150150

151151
deployment_manager = get_deployment_manager(config.target, config.endpoint)
152152
env_dicts = _build_env_dicts(
@@ -157,10 +157,9 @@ def _handle_deploy_create(
157157
env_secrets_name=config.env_secrets_name,
158158
)
159159
deployment = Deployment(
160-
name=config.name
161-
or (config.pipeline if config.pipeline else "unnamed-deployment"),
160+
name=config.name or (config.graph if config.graph else "unnamed-deployment"),
162161
namespace="default",
163-
pipeline=config.pipeline,
162+
graph=config.graph,
164163
entry_service=entry_service,
165164
envs=env_dicts,
166165
)
@@ -231,7 +230,7 @@ def _handle_deploy_create(
231230
@app.command()
232231
def create(
233232
ctx: typer.Context,
234-
pipeline: str = typer.Argument(..., help="Dynamo pipeline to deploy"),
233+
graph: str = typer.Argument(..., help="Dynamo graph to deploy"),
235234
name: t.Optional[str] = typer.Option(None, "--name", "-n", help="Deployment name"),
236235
config_file: t.Optional[typer.FileText] = typer.Option(
237236
None, "--config-file", "-f", help="Configuration file path"
@@ -248,7 +247,7 @@ def create(
248247
envs: t.Optional[t.List[str]] = typer.Option(
249248
None,
250249
"--env",
251-
help="Environment variable(s) to set (format: KEY=VALUE). Note: These environment variables will be set on ALL services in your Dynamo pipeline.",
250+
help="Environment variable(s) to set (format: KEY=VALUE). Note: These environment variables will be set on ALL services in your Dynamo graph.",
252251
),
253252
envs_from_secret: t.Optional[t.List[str]] = typer.Option(
254253
None,
@@ -271,7 +270,7 @@ def create(
271270
) -> DeploymentResponse:
272271
"""Create a deployment on Dynamo Cloud."""
273272
config = DeploymentConfig(
274-
pipeline=pipeline,
273+
graph=graph,
275274
endpoint=endpoint,
276275
name=name,
277276
config_file=config_file,
@@ -391,7 +390,7 @@ def update(
391390
envs: t.Optional[t.List[str]] = typer.Option(
392391
None,
393392
"--env",
394-
help="Environment variable(s) to set (format: KEY=VALUE). Note: These environment variables will be set on ALL services in your Dynamo pipeline.",
393+
help="Environment variable(s) to set (format: KEY=VALUE). Note: These environment variables will be set on ALL services in your Dynamo graph.",
395394
),
396395
envs_from_secret: t.Optional[t.List[str]] = typer.Option(
397396
None,
@@ -507,7 +506,7 @@ def delete(
507506

508507
def deploy(
509508
ctx: typer.Context,
510-
pipeline: str = typer.Argument(..., help="Dynamo pipeline to deploy"),
509+
graph: str = typer.Argument(..., help="Dynamo graph to deploy"),
511510
name: t.Optional[str] = typer.Option(None, "--name", "-n", help="Deployment name"),
512511
config_file: t.Optional[typer.FileText] = typer.Option(
513512
None, "--config-file", "-f", help="Configuration file path"
@@ -524,7 +523,7 @@ def deploy(
524523
envs: t.Optional[t.List[str]] = typer.Option(
525524
None,
526525
"--env",
527-
help="Environment variable(s) to set (format: KEY=VALUE). Note: These environment variables will be set on ALL services in your Dynamo pipeline.",
526+
help="Environment variable(s) to set (format: KEY=VALUE). Note: These environment variables will be set on ALL services in your Dynamo graph.",
528527
),
529528
envs_from_secret: t.Optional[t.List[str]] = typer.Option(
530529
None,
@@ -545,9 +544,9 @@ def deploy(
545544
envvar="DYNAMO_ENV_SECRETS",
546545
),
547546
) -> DeploymentResponse:
548-
"""Deploy a Dynamo pipeline (same as deployment create)."""
547+
"""Deploy a Dynamo graph (same as deployment create)."""
549548
config = DeploymentConfig(
550-
pipeline=pipeline,
549+
graph=graph,
551550
endpoint=endpoint,
552551
name=name,
553552
config_file=config_file,

deploy/sdk/src/dynamo/sdk/cli/serve.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@
4646

4747
def serve(
4848
ctx: typer.Context,
49-
dynamo_pipeline: str = typer.Argument(
50-
..., help="The path to the Dynamo pipeline to serve"
51-
),
49+
graph: str = typer.Argument(..., help="The path to the Dynamo graph to serve"),
5250
service_name: str = typer.Option(
5351
"",
5452
help="Only serve the specified service. Don't serve any dependencies of this service.",
@@ -113,9 +111,9 @@ def serve(
113111
case_sensitive=False,
114112
),
115113
):
116-
"""Locally serve a Dynamo pipeline.
114+
"""Locally serve a Dynamo graph.
117115
118-
Starts a local server for the specified Dynamo pipeline.
116+
Starts a local server for the specified Dynamo graph.
119117
"""
120118
from dynamo.runtime.logging import configure_dynamo_logging
121119
from dynamo.sdk.cli.utils import configure_target_environment
@@ -152,8 +150,8 @@ def serve(
152150
os.environ["DYNAMO_SERVICE_CONFIG"] = json.dumps(service_configs)
153151

154152
if working_dir is None:
155-
if os.path.isdir(os.path.expanduser(dynamo_pipeline)):
156-
working_dir = Path(os.path.expanduser(dynamo_pipeline))
153+
if os.path.isdir(os.path.expanduser(graph)):
154+
working_dir = Path(os.path.expanduser(graph))
157155
else:
158156
working_dir = Path(".")
159157

@@ -163,7 +161,7 @@ def serve(
163161
if sys.path[0] != working_dir_str:
164162
sys.path.insert(0, working_dir_str)
165163

166-
svc = find_and_load_service(dynamo_pipeline, working_dir=working_dir)
164+
svc = find_and_load_service(graph, working_dir=working_dir)
167165
logger.debug(f"Loaded service: {svc.name}")
168166
logger.debug("Dependencies: %s", [dep.on.name for dep in svc.dependencies.values()])
169167
LinkedServices.remove_unused_edges()
@@ -181,13 +179,13 @@ def serve(
181179
# Start the service
182180
console.print(
183181
Panel.fit(
184-
f"[bold]Starting Dynamo service:[/bold] [cyan]{dynamo_pipeline}[/cyan]",
182+
f"[bold]Starting Dynamo service:[/bold] [cyan]{graph}[/cyan]",
185183
title="[bold green]Dynamo Serve[/bold green]",
186184
border_style="green",
187185
)
188186
)
189187
serve_dynamo_graph(
190-
dynamo_pipeline,
188+
graph,
191189
working_dir=working_dir_str,
192190
# host=host,
193191
# port=port,

deploy/sdk/src/dynamo/sdk/cli/serving.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def clear_namespace(namespace: str) -> None:
150150

151151

152152
def serve_dynamo_graph(
153-
dynamo_pipeline: str,
153+
graph: str,
154154
working_dir: str | None = None,
155155
dependency_map: dict[str, str] | None = None,
156156
service_name: str = "",
@@ -171,7 +171,7 @@ def serve_dynamo_graph(
171171

172172
namespace: str = ""
173173
env: dict[str, Any] = {}
174-
svc = find_and_load_service(dynamo_pipeline, working_dir)
174+
svc = find_and_load_service(graph, working_dir)
175175
dynamo_path = pathlib.Path(working_dir or ".")
176176

177177
watchers: list[Watcher] = []
@@ -236,7 +236,7 @@ def serve_dynamo_graph(
236236
f"Service {dep_svc.name} is not servable. Please use link to override with a concrete implementation."
237237
)
238238
new_watcher, new_socket, uri = create_dynamo_watcher(
239-
dynamo_pipeline,
239+
graph,
240240
dep_svc,
241241
uds_path,
242242
allocator,
@@ -254,7 +254,7 @@ def serve_dynamo_graph(
254254
dynamo_args = [
255255
"-m",
256256
_DYNAMO_WORKER_SCRIPT,
257-
dynamo_pipeline,
257+
graph,
258258
"--service-name",
259259
svc.name,
260260
"--worker-id",
@@ -410,7 +410,7 @@ def serve_dynamo_graph(
410410
hasattr(svc, "is_dynamo_component")
411411
and svc.is_dynamo_component()
412412
)
413-
else (dynamo_pipeline,)
413+
else (graph,)
414414
),
415415
),
416416
)

0 commit comments

Comments
 (0)