Skip to content

Commit 627ec12

Browse files
authored
Updated Job BYOC API (#899)
2 parents 7e9b454 + b20073a commit 627ec12

File tree

7 files changed

+239
-55
lines changed

7 files changed

+239
-55
lines changed

ads/jobs/builders/infrastructure/dsc_job.py

+29-3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from ads.common.oci_resource import ResourceNotFoundError
3131
from ads.jobs.builders.infrastructure.base import Infrastructure, RunInstance
3232
from ads.jobs.builders.infrastructure.dsc_job_runtime import (
33+
ContainerRuntimeHandler,
3334
DataScienceJobRuntimeManager,
3435
)
3536
from ads.jobs.builders.infrastructure.utils import get_value
@@ -458,14 +459,19 @@ def run(self, **kwargs) -> DataScienceJobRun:
458459
----------
459460
**kwargs :
460461
Keyword arguments for initializing a Data Science Job Run.
461-
The keys can be any keys in supported by OCI JobConfigurationDetails and JobRun, including:
462+
The keys can be any keys in supported by OCI JobConfigurationDetails, OcirContainerJobEnvironmentConfigurationDetails and JobRun, including:
462463
* hyperparameter_values: dict(str, str)
463464
* environment_variables: dict(str, str)
464465
* command_line_arguments: str
465466
* maximum_runtime_in_minutes: int
466467
* display_name: str
467468
* freeform_tags: dict(str, str)
468469
* defined_tags: dict(str, dict(str, object))
470+
* image: str
471+
* cmd: list[str]
472+
* entrypoint: list[str]
473+
* image_digest: str
474+
* image_signature_id: str
469475
470476
If display_name is not specified, it will be generated as "<JOB_NAME>-run-<TIMESTAMP>".
471477
@@ -478,14 +484,28 @@ def run(self, **kwargs) -> DataScienceJobRun:
478484
if not self.id:
479485
self.create()
480486

481-
swagger_types = (
487+
config_swagger_types = (
482488
oci.data_science.models.DefaultJobConfigurationDetails().swagger_types.keys()
483489
)
490+
env_config_swagger_types = {}
491+
if hasattr(oci.data_science.models, "OcirContainerJobEnvironmentConfigurationDetails"):
492+
env_config_swagger_types = (
493+
oci.data_science.models.OcirContainerJobEnvironmentConfigurationDetails().swagger_types.keys()
494+
)
484495
config_kwargs = {}
496+
env_config_kwargs = {}
485497
keys = list(kwargs.keys())
486498
for key in keys:
487-
if key in swagger_types:
499+
if key in config_swagger_types:
488500
config_kwargs[key] = kwargs.pop(key)
501+
elif key in env_config_swagger_types:
502+
value = kwargs.pop(key)
503+
if key in [
504+
ContainerRuntime.CONST_CMD,
505+
ContainerRuntime.CONST_ENTRYPOINT
506+
] and isinstance(value, str):
507+
value = ContainerRuntimeHandler.split_args(value)
508+
env_config_kwargs[key] = value
489509

490510
# remove timestamp from the job name (added in default names, when display_name not specified by user)
491511
if self.display_name:
@@ -514,6 +534,12 @@ def run(self, **kwargs) -> DataScienceJobRun:
514534
config_override.update(config_kwargs)
515535
kwargs["job_configuration_override_details"] = config_override
516536

537+
if env_config_kwargs:
538+
env_config_kwargs["jobEnvironmentType"] = "OCIR_CONTAINER"
539+
env_config_override = kwargs.get("job_environment_configuration_override_details", {})
540+
env_config_override.update(env_config_kwargs)
541+
kwargs["job_environment_configuration_override_details"] = env_config_override
542+
517543
wait = kwargs.pop("wait", False)
518544
run = DataScienceJobRun(**kwargs, **self.auth).create()
519545
if wait:

ads/jobs/builders/infrastructure/dsc_job_runtime.py

+74-27
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8; -*-
33

4-
# Copyright (c) 2021, 2023 Oracle and/or its affiliates.
4+
# Copyright (c) 2021, 2024 Oracle and/or its affiliates.
55
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
66
"""Contains classes for conversion between ADS runtime and OCI Data Science Job implementation.
77
This module is for ADS developers only.
@@ -305,10 +305,29 @@ def extract(self, dsc_job):
305305
self._extract_envs,
306306
self._extract_artifact,
307307
self._extract_runtime_minutes,
308+
self._extract_properties,
308309
]
309310
for extraction in extractions:
310311
runtime_spec.update(extraction(dsc_job))
311312
return self.RUNTIME_CLASS(self._format_env_var(runtime_spec))
313+
314+
def _extract_properties(self, dsc_job) -> dict:
315+
"""Extract the job runtime properties from data science job.
316+
317+
This is the base method which does not extract the job runtime properties.
318+
Sub-class should implement the extraction if needed.
319+
320+
Parameters
321+
----------
322+
dsc_job : DSCJob or oci.datascience.models.Job
323+
The data science job containing runtime information.
324+
325+
Returns
326+
-------
327+
dict
328+
A runtime specification dictionary for initializing a runtime.
329+
"""
330+
return {}
312331

313332
def _extract_args(self, dsc_job) -> dict:
314333
"""Extracts the command line arguments from data science job.
@@ -942,9 +961,12 @@ def _extract_artifact(self, dsc_job):
942961
class ContainerRuntimeHandler(RuntimeHandler):
943962
RUNTIME_CLASS = ContainerRuntime
944963
CMD_DELIMITER = ","
945-
CONST_CONTAINER_IMAGE = "CONTAINER_CUSTOM_IMAGE"
946-
CONST_CONTAINER_ENTRYPOINT = "CONTAINER_ENTRYPOINT"
947-
CONST_CONTAINER_CMD = "CONTAINER_CMD"
964+
965+
def translate(self, runtime: Runtime) -> dict:
966+
payload = super().translate(runtime)
967+
job_env_config = self._translate_env_config(runtime)
968+
payload["job_environment_configuration_details"] = job_env_config
969+
return payload
948970

949971
def _translate_artifact(self, runtime: Runtime):
950972
"""Specifies a dummy script as the job artifact.
@@ -964,29 +986,34 @@ def _translate_artifact(self, runtime: Runtime):
964986
os.path.dirname(__file__), "../../templates", "container.py"
965987
)
966988

967-
def _translate_env(self, runtime: ContainerRuntime) -> dict:
968-
"""Translate the environment variable.
989+
def _translate_env_config(self, runtime: Runtime) -> dict:
990+
"""Converts runtime properties to ``OcirContainerJobEnvironmentConfigurationDetails`` payload required by OCI Data Science job.
969991
970992
Parameters
971993
----------
972-
runtime : GitPythonRuntime
973-
An instance of GitPythonRuntime
994+
runtime : Runtime
995+
The runtime containing the properties to be converted.
974996
975997
Returns
976998
-------
977999
dict
978-
A dictionary containing environment variables for OCI data science job.
1000+
A dictionary storing the ``OcirContainerJobEnvironmentConfigurationDetails`` payload for OCI data science job.
9791001
"""
980-
if not runtime.image:
981-
raise ValueError("Specify container image for ContainerRuntime.")
982-
envs = super()._translate_env(runtime)
983-
spec_mappings = {
984-
ContainerRuntime.CONST_IMAGE: self.CONST_CONTAINER_IMAGE,
985-
ContainerRuntime.CONST_ENTRYPOINT: self.CONST_CONTAINER_ENTRYPOINT,
986-
ContainerRuntime.CONST_CMD: self.CONST_CONTAINER_CMD,
1002+
job_environment_configuration_details = {
1003+
"job_environment_type": runtime.job_env_type
9871004
}
988-
envs.update(self._translate_specs(runtime, spec_mappings, self.CMD_DELIMITER))
989-
return envs
1005+
1006+
for key, value in ContainerRuntime.attribute_map.items():
1007+
property = runtime.get_spec(key, None)
1008+
if key in [
1009+
ContainerRuntime.CONST_CMD,
1010+
ContainerRuntime.CONST_ENTRYPOINT
1011+
] and isinstance(property, str):
1012+
property = self.split_args(property)
1013+
if property is not None:
1014+
job_environment_configuration_details[value] = property
1015+
1016+
return job_environment_configuration_details
9901017

9911018
@staticmethod
9921019
def split_args(args: str) -> list:
@@ -1031,17 +1058,37 @@ def _extract_envs(self, dsc_job):
10311058
"""
10321059
spec = super()._extract_envs(dsc_job)
10331060
envs = spec.pop(ContainerRuntime.CONST_ENV_VAR, {})
1034-
if self.CONST_CONTAINER_IMAGE not in envs:
1035-
raise IncompatibleRuntime()
1036-
spec[ContainerRuntime.CONST_IMAGE] = envs.pop(self.CONST_CONTAINER_IMAGE)
1037-
cmd = self.split_args(envs.pop(self.CONST_CONTAINER_CMD, ""))
1038-
if cmd:
1039-
spec[ContainerRuntime.CONST_CMD] = cmd
1040-
entrypoint = self.split_args(envs.pop(self.CONST_CONTAINER_ENTRYPOINT, ""))
1041-
if entrypoint:
1042-
spec[ContainerRuntime.CONST_ENTRYPOINT] = entrypoint
1061+
10431062
if envs:
10441063
spec[ContainerRuntime.CONST_ENV_VAR] = envs
1064+
1065+
return spec
1066+
1067+
def _extract_properties(self, dsc_job) -> dict:
1068+
"""Extract the runtime properties from data science job.
1069+
1070+
Parameters
1071+
----------
1072+
dsc_job : DSCJob or oci.datascience.models.Job
1073+
The data science job containing runtime information.
1074+
1075+
Returns
1076+
-------
1077+
dict
1078+
A runtime specification dictionary for initializing a runtime.
1079+
"""
1080+
spec = super()._extract_envs(dsc_job)
1081+
1082+
job_env_config = getattr(dsc_job, "job_environment_configuration_details", None)
1083+
job_env_type = getattr(job_env_config, "job_environment_type", None)
1084+
1085+
if not (job_env_config and job_env_type == "OCIR_CONTAINER"):
1086+
raise IncompatibleRuntime()
1087+
1088+
for key, value in ContainerRuntime.attribute_map.items():
1089+
property = getattr(job_env_config, value, None)
1090+
if property is not None:
1091+
spec[key] = property
10451092
return spec
10461093

10471094

ads/jobs/builders/runtimes/container_runtime.py

+83-4
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,36 @@
33

44
# Copyright (c) 2021, 2024 Oracle and/or its affiliates.
55
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
6+
import logging
67
from typing import Union
78
from ads.jobs.builders.runtimes.base import MultiNodeRuntime
89

10+
logger = logging.getLogger(__name__)
11+
912

1013
class ContainerRuntime(MultiNodeRuntime):
1114
"""Represents a container job runtime
1215
1316
To define container runtime:
1417
1518
>>> ContainerRuntime()
16-
>>> .with_image("iad.ocir.io/<your_tenancy>/<your_image>")
19+
>>> .with_image("iad.ocir.io/<your_tenancy>/<your_image>:<tag>")
1720
>>> .with_cmd("sleep 5 && echo Hello World")
1821
>>> .with_entrypoint(["/bin/sh", "-c"])
22+
>>> .with_image_digest("<image_digest>")
23+
>>> .with_image_signature_id("<image_signature_id>")
1924
>>> .with_environment_variable(MY_ENV="MY_VALUE")
2025
21-
Alternatively, you can define the ``entrypoint`` and ``cmd`` along with the image.
26+
Alternatively, you can define the ``entrypoint``, ``cmd``,
27+
``image_digest``and ``image_signature_id`` along with the image.
2228
2329
>>> ContainerRuntime()
2430
>>> .with_image(
25-
>>> "iad.ocir.io/<your_tenancy>/<your_image>",
31+
>>> "iad.ocir.io/<your_tenancy>/<your_image>:<tag>",
2632
>>> entrypoint=["/bin/sh", "-c"],
2733
>>> cmd="sleep 5 && echo Hello World",
34+
>>> image_digest="<image_digest>",
35+
>>> image_signature_id="<image_signature_id>",
2836
>>> )
2937
>>> .with_environment_variable(MY_ENV="MY_VALUE")
3038
@@ -46,20 +54,34 @@ class ContainerRuntime(MultiNodeRuntime):
4654
CONST_IMAGE = "image"
4755
CONST_ENTRYPOINT = "entrypoint"
4856
CONST_CMD = "cmd"
57+
CONST_IMAGE_DIGEST = "imageDigest"
58+
CONST_IMAGE_SIGNATURE_ID = "imageSignatureId"
4959
attribute_map = {
5060
CONST_IMAGE: CONST_IMAGE,
5161
CONST_ENTRYPOINT: CONST_ENTRYPOINT,
5262
CONST_CMD: CONST_CMD,
63+
CONST_IMAGE_DIGEST: "image_digest",
64+
CONST_IMAGE_SIGNATURE_ID: "image_signature_id",
5365
}
5466
attribute_map.update(MultiNodeRuntime.attribute_map)
5567

68+
@property
69+
def job_env_type(self) -> str:
70+
"""The container type"""
71+
return "OCIR_CONTAINER"
72+
5673
@property
5774
def image(self) -> str:
5875
"""The container image"""
5976
return self.get_spec(self.CONST_IMAGE)
6077

6178
def with_image(
62-
self, image: str, entrypoint: Union[str, list, None] = None, cmd: str = None
79+
self,
80+
image: str,
81+
entrypoint: Union[str, list, None] = None,
82+
cmd: str = None,
83+
image_digest: str = None,
84+
image_signature_id: str = None,
6385
) -> "ContainerRuntime":
6486
"""Specify the image for the container job.
6587
@@ -71,16 +93,73 @@ def with_image(
7193
Entrypoint for the job, by default None (the entrypoint defined in the image will be used).
7294
cmd : str, optional
7395
Command for the job, by default None.
96+
image_digest: str, optional
97+
The image digest, by default None.
98+
image_signature_id: str, optional
99+
The image signature id, by default None.
74100
75101
Returns
76102
-------
77103
ContainerRuntime
78104
The runtime instance.
79105
"""
106+
if not isinstance(image, str):
107+
raise ValueError(
108+
"Custom image must be provided as a string."
109+
)
110+
if image.find(":") < 0:
111+
logger.warning(
112+
"Tag is required for custom image. Accepted format: iad.ocir.io/<tenancy>/<image>:<tag>."
113+
)
80114
self.with_entrypoint(entrypoint)
81115
self.set_spec(self.CONST_CMD, cmd)
116+
self.with_image_digest(image_digest)
117+
self.with_image_signature_id(image_signature_id)
82118
return self.set_spec(self.CONST_IMAGE, image)
83119

120+
@property
121+
def image_digest(self) -> str:
122+
"""The container image digest."""
123+
return self.get_spec(self.CONST_IMAGE_DIGEST)
124+
125+
def with_image_digest(self, image_digest: str) -> "ContainerRuntime":
126+
"""Sets the digest of custom image.
127+
128+
Parameters
129+
----------
130+
image_digest: str
131+
The image digest.
132+
133+
Returns
134+
-------
135+
ContainerRuntime
136+
The runtime instance.
137+
"""
138+
return self.set_spec(self.CONST_IMAGE_DIGEST, image_digest)
139+
140+
@property
141+
def image_signature_id(self) -> str:
142+
"""The container image signature id."""
143+
return self.get_spec(self.CONST_IMAGE_SIGNATURE_ID)
144+
145+
def with_image_signature_id(self, image_signature_id: str) -> "ContainerRuntime":
146+
"""Sets the signature id of custom image.
147+
148+
Parameters
149+
----------
150+
image_signature_id: str
151+
The image signature id.
152+
153+
Returns
154+
-------
155+
ContainerRuntime
156+
The runtime instance.
157+
"""
158+
return self.set_spec(
159+
self.CONST_IMAGE_SIGNATURE_ID,
160+
image_signature_id
161+
)
162+
84163
@property
85164
def entrypoint(self) -> str:
86165
"""Entrypoint of the container job"""

docs/source/user_guide/jobs/run_container.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Here is an example to create and run a container job:
2222

2323
To configure ``ContainerRuntime``, you must specify the container ``image``.
2424
Similar to other runtime, you can add environment variables.
25-
You can optionally specify the `entrypoint` and `cmd` for running the container.
25+
You can optionally specify the `entrypoint`, `cmd`, `image_digest` and `image_signature_id` for running the container.
2626

2727
See also:
2828

docs/source/user_guide/jobs/tabs/container_runtime.rst

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
)
2828
.with_runtime(
2929
ContainerRuntime()
30-
.with_image("<region>.ocir.io/<your_tenancy>/<your_image>")
30+
.with_image("<region>.ocir.io/<tenancy>/<image>:<tag>")
31+
.with_image_digest("<image_digest>")
32+
.with_image_signature_id("<image_signature_id>")
3133
.with_environment_variable(GREETINGS="Welcome to OCI Data Science")
3234
.with_entrypoint(["/bin/sh", "-c"])
3335
.with_cmd("sleep 5 && echo $GREETINGS")

0 commit comments

Comments
 (0)