Skip to content

Commit 1bf2d9e

Browse files
authored
Follow up fixes for deployments (#4013)
1 parent 65be296 commit 1bf2d9e

File tree

21 files changed

+230
-262
lines changed

21 files changed

+230
-262
lines changed

src/zenml/artifact_stores/base_artifact_store.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from zenml.constants import (
3939
ENV_ZENML_SERVER,
4040
ENV_ZENML_SERVER_ALLOW_LOCAL_FILE_ACCESS,
41+
IN_MEMORY_ARTIFACT_URI_PREFIX,
4142
handle_bool_env_var,
4243
)
4344
from zenml.enums import StackComponentType
@@ -106,8 +107,8 @@ def _validate_path(self, path: str) -> None:
106107
IllegalOperationError: If the path is a local file and the server
107108
is not configured to allow local file access.
108109
"""
109-
# Skip validation for memory:// URIs used in serving mode
110-
if path.startswith("memory://"):
110+
if path.startswith(IN_MEMORY_ARTIFACT_URI_PREFIX):
111+
# No need to validate in-memory URIs
111112
return
112113

113114
if not self.allow_local_file_access and not io_utils.is_remote(path):
@@ -143,9 +144,7 @@ def _sanitize_potential_path(self, potential_path: Any) -> Any:
143144
# Neither string nor bytes, this is not a path
144145
return potential_path
145146

146-
# Preserve special in-memory scheme used by serving mode as-is
147-
# to avoid treating it as a local filesystem path.
148-
if isinstance(path, str) and path.startswith("memory://"):
147+
if path.startswith(IN_MEMORY_ARTIFACT_URI_PREFIX):
149148
return path
150149

151150
if io_utils.is_remote(path):

src/zenml/artifacts/utils.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from zenml.client import Client
3939
from zenml.constants import (
4040
ENV_ZENML_SERVER,
41+
IN_MEMORY_ARTIFACT_URI_PREFIX,
4142
MODEL_METADATA_YAML_FILE_NAME,
4243
)
4344
from zenml.enums import (
@@ -54,6 +55,9 @@
5455
)
5556
from zenml.io import fileio
5657
from zenml.logger import get_logger
58+
from zenml.materializers.in_memory_materializer import (
59+
InMemoryMaterializer,
60+
)
5761
from zenml.metadata.metadata_types import validate_metadata
5862
from zenml.models import (
5963
ArtifactVersionRequest,
@@ -153,20 +157,14 @@ def _store_artifact_data_and_prepare_request(
153157
"""
154158
artifact_store = Client().active_stack.artifact_store
155159

156-
# Detect in-memory materializer to avoid touching the artifact store.
157-
# Local import to minimize import-time dependencies.
158-
from zenml.materializers.in_memory_materializer import (
159-
InMemoryMaterializer,
160+
uses_in_memory_materializer = issubclass(
161+
materializer_class, InMemoryMaterializer
160162
)
161163

162-
is_in_memory = issubclass(materializer_class, InMemoryMaterializer)
163-
164-
if not is_in_memory:
164+
if not uses_in_memory_materializer:
165165
artifact_store.makedirs(uri)
166-
else:
167-
# Ensure URI clearly indicates in-memory storage and not the artifact store
168-
if not uri.startswith("memory://"):
169-
uri = f"memory://custom_artifacts/{name}/{uuid4()}"
166+
elif not uri.startswith(IN_MEMORY_ARTIFACT_URI_PREFIX):
167+
uri = f"{IN_MEMORY_ARTIFACT_URI_PREFIX}{name}/{uuid4()}"
170168

171169
materializer = materializer_class(uri=uri, artifact_store=artifact_store)
172170
materializer.uri = materializer.uri.replace("\\", "/")
@@ -204,7 +202,9 @@ def _store_artifact_data_and_prepare_request(
204202
data_type=source_utils.resolve(data_type),
205203
content_hash=content_hash,
206204
project=Client().active_project.id,
207-
artifact_store_id=None if is_in_memory else artifact_store.id,
205+
artifact_store_id=None
206+
if uses_in_memory_materializer
207+
else artifact_store.id,
208208
visualizations=visualizations,
209209
has_custom_name=has_custom_name,
210210
save_type=save_type,

src/zenml/cli/deployment.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def list_deployments(**kwargs: Any) -> None:
9393
cli_utils.print_page_info(deployments)
9494

9595

96-
@deployment.command("describe")
96+
@deployment.command("describe", help="Describe a deployment.")
9797
@click.argument("deployment_name_or_id", type=str, required=True)
9898
@click.option(
9999
"--show-secret",
@@ -151,7 +151,7 @@ def describe_deployment(
151151
)
152152

153153

154-
@deployment.command("provision")
154+
@deployment.command("provision", help="Provision a deployment.")
155155
@click.argument("deployment_name_or_id", type=str, required=True)
156156
@click.option(
157157
"--snapshot",
@@ -253,7 +253,7 @@ def provision_deployment(
253253
cli_utils.pretty_print_deployment(deployment, show_secret=True)
254254

255255

256-
@deployment.command("deprovision")
256+
@deployment.command("deprovision", help="Deprovision a deployment.")
257257
@click.argument("deployment_name_or_id", type=str, required=False)
258258
@click.option(
259259
"--all",
@@ -408,7 +408,7 @@ def deprovision_deployment(
408408
cli_utils.error(error_message)
409409

410410

411-
@deployment.command("delete")
411+
@deployment.command("delete", help="Delete a deployment.")
412412
@click.argument("deployment_name_or_id", type=str, required=False)
413413
@click.option(
414414
"--all",
@@ -564,7 +564,7 @@ def delete_deployment(
564564
cli_utils.error(error_message)
565565

566566

567-
@deployment.command("refresh")
567+
@deployment.command("refresh", help="Refresh the status of a deployment.")
568568
@click.argument("deployment_name_or_id", type=str, required=True)
569569
def refresh_deployment(
570570
deployment_name_or_id: str,
@@ -586,7 +586,9 @@ def refresh_deployment(
586586

587587

588588
@deployment.command(
589-
"invoke", context_settings={"ignore_unknown_options": True}
589+
"invoke",
590+
context_settings={"ignore_unknown_options": True},
591+
help="Invoke a deployment with arguments.",
590592
)
591593
@click.argument("deployment_name_or_id", type=str, required=True)
592594
@click.option(
@@ -656,7 +658,7 @@ def invoke_deployment(
656658
)
657659

658660

659-
@deployment.command("logs")
661+
@deployment.command("logs", help="Get the logs of a deployment.")
660662
@click.argument("deployment_name_or_id", type=str, required=True)
661663
@click.option(
662664
"--follow",
@@ -694,8 +696,5 @@ def log_deployment(
694696
except KeyError as e:
695697
cli_utils.error(str(e))
696698
else:
697-
with console.status(
698-
f"Streaming logs for deployment '{deployment_name_or_id}'...\n"
699-
):
700-
for log in logs:
701-
print(log)
699+
for log in logs:
700+
print(log)

src/zenml/cli/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2383,7 +2383,7 @@ def get_deployment_status_emoji(
23832383
if status == DeploymentStatus.ERROR:
23842384
return ":x:"
23852385
if status == DeploymentStatus.RUNNING:
2386-
return ":gear:"
2386+
return ":green_circle:"
23872387
if status == DeploymentStatus.ABSENT:
23882388
return ":stop_sign:"
23892389

@@ -2437,6 +2437,7 @@ def print_deployment_table(
24372437
status = deployment.status or DeploymentStatus.UNKNOWN.value
24382438
status_emoji = get_deployment_status_emoji(status)
24392439
run_dict = {
2440+
"ID": deployment.id,
24402441
"NAME": deployment.name,
24412442
"PIPELINE": pipeline_name,
24422443
"SNAPSHOT": deployment.snapshot.name or ""

src/zenml/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@ def handle_int_env_var(var: str, default: int = 0) -> int:
461461

462462
# deployer constants
463463
DEPLOYER_DOCKER_IMAGE_KEY = "deployer"
464+
IN_MEMORY_ARTIFACT_URI_PREFIX = "memory://"
464465

465466
# Secret constants
466467
SECRET_VALUES = "values"

src/zenml/deployers/containerized_deployer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
1212
# or implied. See the License for the specific language governing
1313
# permissions and limitations under the License.
14-
"""Base class for all ZenML deployers."""
14+
"""Base class for all containerized deployers."""
1515

1616
from abc import ABC
1717
from typing import (

src/zenml/deployers/docker/docker_deployer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ def do_provision_deployment(
365365
preferred_ports=preferred_ports,
366366
allocate_port_if_busy=settings.allocate_port_if_busy,
367367
range=settings.port_range,
368+
address="0.0.0.0", # nosec
368369
)
369370
ports: Dict[str, Optional[int]] = {"8000/tcp": port}
370371

src/zenml/deployers/server/runtime.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ def reset(self) -> None:
4848
self.active = False
4949
self.request_id = None
5050
self.snapshot_id = None
51-
self.pipeline_parameters.clear()
52-
self.outputs.clear()
51+
self.pipeline_parameters = {}
52+
self.outputs = {}
5353
self.skip_artifact_materialization = False
54-
self.in_memory_data.clear()
54+
self.in_memory_data = {}
5555

5656

5757
_deployment_context: contextvars.ContextVar[_DeploymentState] = (
@@ -166,5 +166,5 @@ def get_in_memory_data(uri: str) -> Any:
166166
"""
167167
if is_active():
168168
state = _get_context()
169-
return state.in_memory_data.get(uri)
169+
return state.in_memory_data[uri]
170170
return None

src/zenml/deployers/server/service.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
ServiceInfo,
3636
SnapshotInfo,
3737
)
38+
from zenml.deployers.utils import (
39+
deployment_snapshot_request_from_source_snapshot,
40+
)
3841
from zenml.enums import StackComponentType
3942
from zenml.hooks.hook_validators import load_and_run_hook
4043
from zenml.logger import get_logger
@@ -337,11 +340,6 @@ def _prepare_execute_with_orchestrator(
337340
Returns:
338341
A tuple of (placeholder_run, deployment_snapshot).
339342
"""
340-
# Create a new snapshot with deployment-specific parameters and settings
341-
from zenml.orchestrators.utils import (
342-
deployment_snapshot_request_from_source_snapshot,
343-
)
344-
345343
deployment_snapshot_request = (
346344
deployment_snapshot_request_from_source_snapshot(
347345
source_snapshot=self.snapshot,

src/zenml/deployers/utils.py

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,21 @@
2121
import requests
2222

2323
from zenml.client import Client
24+
from zenml.config.step_configurations import Step
2425
from zenml.deployers.exceptions import (
2526
DeploymentHTTPError,
2627
DeploymentNotFoundError,
2728
DeploymentProvisionError,
2829
)
2930
from zenml.enums import DeploymentStatus
30-
from zenml.models import DeploymentResponse
31+
from zenml.models import (
32+
CodeReferenceRequest,
33+
DeploymentResponse,
34+
PipelineSnapshotRequest,
35+
PipelineSnapshotResponse,
36+
)
3137
from zenml.steps.step_context import get_step_context
38+
from zenml.utils import pydantic_utils
3239
from zenml.utils.json_utils import pydantic_encoder
3340

3441

@@ -274,3 +281,120 @@ def invoke_deployment(
274281
raise DeploymentHTTPError(
275282
f"Request failed for deployment {deployment_name_or_id}: {e}"
276283
)
284+
285+
286+
def deployment_snapshot_request_from_source_snapshot(
287+
source_snapshot: PipelineSnapshotResponse,
288+
deployment_parameters: Dict[str, Any],
289+
) -> PipelineSnapshotRequest:
290+
"""Generate a snapshot request for deployment execution.
291+
292+
Args:
293+
source_snapshot: The source snapshot from which to create the
294+
snapshot request.
295+
deployment_parameters: Parameters to override for deployment execution.
296+
297+
Raises:
298+
RuntimeError: If the source snapshot does not have an associated stack.
299+
300+
Returns:
301+
The generated snapshot request.
302+
"""
303+
if source_snapshot.stack is None:
304+
raise RuntimeError("Missing source snapshot stack")
305+
306+
pipeline_configuration = pydantic_utils.update_model(
307+
source_snapshot.pipeline_configuration, {"enable_cache": False}
308+
)
309+
310+
steps = {}
311+
for invocation_id, step in source_snapshot.step_configurations.items():
312+
updated_step_parameters = step.config.parameters.copy()
313+
314+
for param_name in step.config.parameters:
315+
if param_name in deployment_parameters:
316+
updated_step_parameters[param_name] = deployment_parameters[
317+
param_name
318+
]
319+
320+
# Deployment-specific step overrides
321+
step_update = {
322+
"enable_cache": False, # Disable caching for all steps
323+
"step_operator": None, # Remove step operators for deployments
324+
"retry": None, # Remove retry configuration
325+
"parameters": updated_step_parameters,
326+
}
327+
328+
step_config = pydantic_utils.update_model(
329+
step.step_config_overrides, step_update
330+
)
331+
merged_step_config = step_config.apply_pipeline_configuration(
332+
pipeline_configuration
333+
)
334+
335+
steps[invocation_id] = Step(
336+
spec=step.spec,
337+
config=merged_step_config,
338+
step_config_overrides=step_config,
339+
)
340+
341+
code_reference_request = None
342+
if source_snapshot.code_reference:
343+
code_reference_request = CodeReferenceRequest(
344+
commit=source_snapshot.code_reference.commit,
345+
subdirectory=source_snapshot.code_reference.subdirectory,
346+
code_repository=source_snapshot.code_reference.code_repository.id,
347+
)
348+
349+
zenml_version = Client().zen_store.get_store_info().version
350+
351+
# Compute the source snapshot ID:
352+
# - If the source snapshot has a name, we use it as the source snapshot.
353+
# That way, all runs will be associated with this snapshot.
354+
# - If the source snapshot is based on another snapshot (which therefore
355+
# has a name), we use that one instead.
356+
# - If the source snapshot does not have a name and is not based on another
357+
# snapshot, we don't set a source snapshot.
358+
#
359+
# With this, we ensure that all runs are associated with the closest named
360+
# source snapshot.
361+
source_snapshot_id = None
362+
if source_snapshot.name:
363+
source_snapshot_id = source_snapshot.id
364+
elif source_snapshot.source_snapshot_id:
365+
source_snapshot_id = source_snapshot.source_snapshot_id
366+
367+
updated_pipeline_spec = source_snapshot.pipeline_spec
368+
if (
369+
source_snapshot.pipeline_spec
370+
and source_snapshot.pipeline_spec.parameters is not None
371+
):
372+
original_params: Dict[str, Any] = dict(
373+
source_snapshot.pipeline_spec.parameters
374+
)
375+
merged_params: Dict[str, Any] = original_params.copy()
376+
for k, v in deployment_parameters.items():
377+
if k in original_params:
378+
merged_params[k] = v
379+
updated_pipeline_spec = pydantic_utils.update_model(
380+
source_snapshot.pipeline_spec, {"parameters": merged_params}
381+
)
382+
383+
return PipelineSnapshotRequest(
384+
project=source_snapshot.project_id,
385+
run_name_template=source_snapshot.run_name_template,
386+
pipeline_configuration=pipeline_configuration,
387+
step_configurations=steps,
388+
client_environment={},
389+
client_version=zenml_version,
390+
server_version=zenml_version,
391+
stack=source_snapshot.stack.id,
392+
pipeline=source_snapshot.pipeline.id,
393+
schedule=None,
394+
code_reference=code_reference_request,
395+
code_path=source_snapshot.code_path,
396+
build=source_snapshot.build.id if source_snapshot.build else None,
397+
source_snapshot=source_snapshot_id,
398+
pipeline_version_hash=source_snapshot.pipeline_version_hash,
399+
pipeline_spec=updated_pipeline_spec,
400+
)

0 commit comments

Comments
 (0)