Skip to content

Commit 3d1aa9b

Browse files
committed
Add mout section to allow specifying uploads via polyaxonfile
* Allow uploading/mounting multiple folders via cli
1 parent e567fed commit 3d1aa9b

10 files changed

Lines changed: 183 additions & 15 deletions

File tree

cli/polyaxon/_cli/run.py

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from polyaxon._flow import V1Operation, V1RunPending
2929
from polyaxon._managers.git import GitConfigManager
3030
from polyaxon._managers.run import RunConfigManager
31-
from polyaxon._polyaxonfile import check_polyaxonfile
31+
from polyaxon._polyaxonfile import check_polyaxonfile, CompiledOperationSpecification
3232
from polyaxon._runner.kinds import RunnerKind
3333
from polyaxon._schemas.lifecycle import ManagedBy
3434
from polyaxon._utils import cache
@@ -161,14 +161,31 @@ def start_run_shell(run_uuid: str):
161161
}
162162
ctx.invoke(run_shell)
163163

164-
def upload_run(run_uuid: str):
164+
def upload_run(run_uuid: str, path_from: str, path_to: str):
165165
ctx.obj = {
166166
"project": "{}/{}".format(owner_team, project_name),
167167
"run_uuid": run_uuid,
168168
}
169169
ctx.invoke(
170-
run_upload, path_to=upload_to, path_from=upload_from, sync_failure=True
170+
run_upload,
171+
path_to=path_to,
172+
path_from=path_from,
173+
sync_failure=True,
171174
)
175+
176+
def upload_mounts(run_uuid: str):
177+
if upload:
178+
upload_run(
179+
build_uuid or run_instance.uuid,
180+
path_to=upload_to,
181+
path_from=upload_from,
182+
)
183+
184+
compiled_spec = CompiledOperationSpecification.read(run_instance.content)
185+
mounts = compiled_spec.get_resolved_mount()
186+
for mount in mounts:
187+
upload_run(run_uuid, path_from=mount.path_from, path_to=mount.path_to)
188+
172189
if approve_after and approve_after > 0:
173190
Printer.print(f"Waiting {approve_after}s to approve the run...")
174191
time.sleep(approve_after)
@@ -184,10 +201,12 @@ def upload_run(run_uuid: str):
184201
upload_to = upload_to or DEFAULT_UPLOADS_PATH
185202
run_meta_info = run_meta_info or {}
186203
run_meta_info[META_UPLOAD_ARTIFACTS] = upload_to
204+
205+
is_uploading = upload or op_spec.has_mount()
187206
run_instance = create_run(
188207
managed_by=managed_by,
189208
meta_info=run_meta_info,
190-
pending=V1RunPending.UPLOAD if upload else None,
209+
pending=V1RunPending.UPLOAD if is_uploading else None,
191210
)
192211
if not run_instance:
193212
return
@@ -200,8 +219,8 @@ def upload_run(run_uuid: str):
200219
build_name = run_instance.settings.build.get("name")
201220
runs_to_watch.insert(0, RunWatchSpec(build_uuid, build_name))
202221

203-
if upload:
204-
upload_run(build_uuid or run_instance.uuid)
222+
if is_uploading:
223+
upload_mounts(build_uuid or run_instance.uuid)
205224

206225
if local:
207226
for instance in runs_to_watch:
@@ -277,8 +296,20 @@ def upload_run(run_uuid: str):
277296
"-u",
278297
is_flag=True,
279298
default=False,
280-
help="To upload the working dir to run's artifacts path "
281-
"as an init context before scheduling the run.",
299+
help="Upload the current working directory to run's artifacts path "
300+
"as an init context before scheduling the run. "
301+
"Uploads to 'uploads/' by default.",
302+
)
303+
@click.option(
304+
"--mount",
305+
"-m",
306+
"mounts",
307+
multiple=True,
308+
type=str,
309+
help="Mount local paths to run artifacts. "
310+
"Syntax: './src:dest' or './src' (defaults to 'uploads/' destination). "
311+
"Can be specified multiple times. "
312+
"Examples: -m ./code -m ./data:datasets -m ./models:models",
282313
)
283314
@click.option(
284315
"--upload-from",
@@ -437,6 +468,7 @@ def run(
437468
shell,
438469
log,
439470
upload,
471+
mounts,
440472
upload_from,
441473
upload_to,
442474
watch,
@@ -498,21 +530,20 @@ def run(
498530
$ polyaxon run -pm path/to/my-component.py:componentA
499531
500532
501-
Uploading from everything in the current folder to the default uploads path
533+
Uploading current folder to the default uploads path
502534
503535
\b
504536
$ polyaxon run ... -u
505537
506-
507-
Uploading from everything in the current folder to a custom path, e.g. code
538+
Uploading from a subfolder to a custom path
508539
509540
\b
510-
$ polyaxon run ... -u-to code
541+
$ polyaxon run ... -u -u-from ./code -u-to code
511542
512-
Uploading from everything from a sub-folder, e.g. ./code to the a custom path, e.g. new-code
543+
Mounting multiple paths using -m
513544
514545
\b
515-
$ polyaxon run ... -u-from ./code -u-to new-code
546+
$ polyaxon run ... -m ./src:code -m ./data:datasets -m ./models
516547
"""
517548
if log and shell:
518549
Printer.error(
@@ -590,6 +621,13 @@ def run(
590621
owner, team, project_name = get_project_or_local(project, is_cli=True)
591622
tags = validate_tags(tags, validate_yaml=True)
592623

624+
# Handle CLI uploads - merge with polyaxonfile mount section
625+
cli_mounts = list(mounts) if mounts else []
626+
627+
if cli_mounts:
628+
existing_mount = list(op_spec.mount) if op_spec.mount else []
629+
op_spec.mount = existing_mount + cli_mounts
630+
593631
_run(
594632
ctx=ctx,
595633
name=name,

cli/polyaxon/_flow/component/base.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from polyaxon._flow.builds import V1Build
1515
from polyaxon._flow.cache import V1Cache
1616
from polyaxon._flow.hooks import V1Hook
17+
from polyaxon._schemas.types.mounts import V1Mount
1718
from polyaxon._flow.plugins import V1Plugins
1819
from polyaxon._flow.termination import V1Termination
1920
from polyaxon._schemas.base import BaseSchemaModel
@@ -35,9 +36,25 @@ class BaseComponent(BaseSchemaModel):
3536
hooks: Optional[Union[List[V1Hook], RefField]] = None
3637
is_approved: Optional[BoolOrRef] = Field(alias="isApproved", default=None)
3738
cost: Optional[FloatOrRef] = None
39+
mount: Optional[List[Union[StrictStr, V1Mount]]] = None
3840

3941
@field_validator("tags", "presets", **validation_before)
4042
def validate_str_list(cls, v):
4143
if isinstance(v, str):
4244
return to_list(v, check_str=True)
4345
return v
46+
47+
def get_resolved_mount(self):
48+
if not self.mount:
49+
return []
50+
resolved_mounts = []
51+
for mount in self.mount:
52+
if isinstance(mount, str):
53+
if ":" in mount:
54+
p_from, p_to = mount.split(":", 1)
55+
else:
56+
p_from, p_to = mount, None
57+
resolved_mounts.append(V1Mount(path_from=p_from, path_to=p_to))
58+
else:
59+
resolved_mounts.append(mount)
60+
return resolved_mounts

cli/polyaxon/_flow/component/component.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class V1Component(
4343
cache: [V1Cache](/docs/automation/helpers/cache/), optional
4444
termination: [V1Termination](/docs/core/specification/termination/), optional
4545
plugins: [V1Plugins](/docs/core/specification/plugins/), optional
46+
mount: List[[V1Mount](/docs/core/specification/mount/)], optional
4647
build: [V1Build](/docs/automation/builds/), optional
4748
hooks: List[[V1Hook](/docs/automation/hooks/)], optional
4849
inputs: [V1IO](/docs/core/specification/io/), optional
@@ -69,6 +70,7 @@ class V1Component(
6970
>>> hooks:
7071
>>> inputs:
7172
>>> outputs:
73+
>>> mount:
7274
>>> build:
7375
>>> run:
7476
>>> isApproved:
@@ -94,6 +96,7 @@ class V1Component(
9496
>>> hooks=[V1Hook(...)],
9597
>>> inputs=[V1IO(...)],
9698
>>> outputs=[V1IO(...)],
99+
>>> mount=[V1Mount(...)],
97100
>>> build=V1Build(...),
98101
>>> run=...
99102
>>> )
@@ -317,6 +320,22 @@ class V1Component(
317320
>>> ...
318321
```
319322
323+
### mount
324+
325+
> **Note**: ver 2.13+. Please check [V1Mount](/docs/core/specification/mount/) for more details.
326+
327+
This section defines a list of mounts to be used for this operation.
328+
Mounts can be defined either as strings or as full objects.
329+
```yaml
330+
>>> operation:
331+
>>> ...
332+
>>> mount:
333+
>>> - /path/in/host:/path/in/container # defined as string
334+
>>> - path_from: /path/in/host # defined as object
335+
>>> path_to: /path/in/container
336+
>>> ...
337+
```
338+
320339
### build
321340
322341
> **Note**: Please check [V1Build](/docs/automation/builds/) for more details.

cli/polyaxon/_flow/operations/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515

1616
class BaseOp(BaseComponent, MatrixMixin, ScheduleMixin):
17-
_FIELDS_SAME_KIND_PATCH = ["schedule", "matrix"]
17+
_FIELDS_SAME_KIND_PATCH = ["schedule", "matrix", "mount"]
1818

1919
schedule: Optional[V1Schedule] = None
2020
events: Optional[Union[List[V1EventTrigger], RefField]] = None

cli/polyaxon/_flow/operations/operation.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class V1Operation(BaseOp, TemplateMixinConfig):
5858
params: Dict[str, [V1Param](/docs/core/specification/params/)], optional
5959
schedule: Union[[V1CronSchedule](/docs/automation/schedules/cron/), [V1IntervalSchedule](/docs/automation/schedules/interval/), [V1DateTimeSchedule](/docs/automation/schedules/datetime/)], optional # noqa
6060
events: List[[V1EventTrigger](/docs/automation/events/)], optional
61+
mount: List[[V1Mount](/docs/core/specification/mount/)], optional
6162
build: [V1Build](/docs/automation/builds/), optional
6263
hooks: List[[V1Hook](/docs/automation/hooks/)], optional
6364
matrix: Union[[V1Mapping](/docs/automation/mapping/), [V1GridSearch](/docs/automation/optimization-engine/grid-search/), [V1RandomSearch](/docs/automation/optimization-engine/random-search/), [V1Hyperband](/docs/automation/optimization-engine/hyperband/), [V1Bayes](/docs/automation/optimization-engine/bayesian-optimization/), [V1Hyperopt](/docs/automation/optimization-engine/hyperopt/), [V1Iterative](/docs/automation/optimization-engine/iterative/)], optional # noqa
@@ -96,6 +97,7 @@ class V1Operation(BaseOp, TemplateMixinConfig):
9697
>>> actions:
9798
>>> hooks:
9899
>>> params:
100+
>>> mount:
99101
>>> build:
100102
>>> runPatch:
101103
>>> hubRef:
@@ -126,6 +128,7 @@ class V1Operation(BaseOp, TemplateMixinConfig):
126128
>>> events=["event-ref1", "event-ref2"],
127129
>>> hooks=[V1Hook(...)],
128130
>>> outputs={"param1": V1Param(...), ...},
131+
>>> mount=[V1Mount(...)],
129132
>>> build=V1Build(...),
130133
>>> component=V1Component(...),
131134
>>> )
@@ -328,6 +331,22 @@ class V1Operation(BaseOp, TemplateMixinConfig):
328331
>>> ...
329332
```
330333
334+
### mount
335+
336+
> **Note**: ver 2.13+. Please check [V1Mount](/docs/core/specification/mount/) for more details.
337+
338+
This section defines a list of mounts to be used for this operation.
339+
Mounts can be defined either as strings or as full objects.
340+
```yaml
341+
>>> operation:
342+
>>> ...
343+
>>> mount:
344+
>>> - /path/in/host:/path/in/container # defined as string
345+
>>> - path_from: /path/in/host # defined as object
346+
>>> path_to: /path/in/container
347+
>>> ...
348+
```
349+
331350
### build
332351
333352
> **Note**: Please check [V1Build](/docs/automation/builds/) for more details.
@@ -712,5 +731,12 @@ def from_build(cls, build: V1Build, contexts: Optional[Dict] = None):
712731

713732
return cls.model_construct(**content)
714733

734+
def has_mount(self):
735+
if self.mount:
736+
return True
737+
if self.component and self.component.mount:
738+
return True
739+
return False
740+
715741

716742
PartialV1Operation = to_partial(V1Operation)

cli/polyaxon/_polyaxonfile/specs/compiled_operation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ def apply_preset(
332332
"events",
333333
"plugins",
334334
"termination",
335+
"mount",
335336
"matrix",
336337
"joins",
337338
"schedule",

cli/polyaxon/_polyaxonfile/specs/operation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def get_context_io(c_name: str, c_io: V1Param, is_list=None):
110110
"trigger",
111111
"conditions",
112112
"skip_on_upstream_skip",
113+
"mount",
113114
}
114115
patch_keys = patch_keys.intersection(config.model_fields_set)
115116
patch_data = {k: getattr(config, k) for k in patch_keys}

cli/polyaxon/_polyaxonfile/specs/sections.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class Sections:
3939
PATH_REF = "pathRef"
4040
URL_REF = "urlRef"
4141
COMPONENT = "component"
42+
MOUNT = "mount"
4243

4344
SECTIONS = (
4445
VERSION,
@@ -80,6 +81,7 @@ class Sections:
8081
CONTEXTS,
8182
RUN,
8283
RUN_PATCH,
84+
MOUNT,
8385
)
8486

8587
PARSING_SECTIONS = (
@@ -97,6 +99,7 @@ class Sections:
9799
DEPENDENCIES,
98100
TRIGGER,
99101
CONDITIONS,
102+
MOUNT,
100103
SKIP_ON_UPSTREAM_SKIP,
101104
PATCH_STRATEGY,
102105
"is_approved",
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from typing import Optional
2+
3+
from clipped.compact.pydantic import Field, StrictStr
4+
5+
from polyaxon._schemas.types.base import BaseTypeConfig
6+
7+
8+
class V1Mount(BaseTypeConfig):
9+
_IDENTIFIER = "mount"
10+
11+
path_from: Optional[StrictStr] = Field(alias="from", default=None)
12+
path_to: Optional[StrictStr] = Field(alias="to", default=None)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import pytest
2+
from polyaxon._flow import V1Operation, V1RunKind
3+
from polyaxon._schemas.types.mounts import V1Mount
4+
from polyaxon._utils.test_utils import BaseTestCase
5+
6+
7+
@pytest.mark.polyaxonfile_mark
8+
class TestMounts(BaseTestCase):
9+
def test_mount_short_syntax(self):
10+
config_dict = {
11+
"version": 1.1,
12+
"kind": "operation",
13+
"mount": ["./src:/dst", "./src2"],
14+
"component": {
15+
"run": {"kind": V1RunKind.JOB, "container": {"image": "test"}}
16+
},
17+
}
18+
op_config = V1Operation.read(values=config_dict)
19+
assert op_config.mount == ["./src:/dst", "./src2"]
20+
assert op_config.get_resolved_mount() == [
21+
V1Mount(path_from="./src", path_to="/dst"),
22+
V1Mount(path_from="./src2", path_to=None),
23+
]
24+
25+
def test_mount_long_syntax(self):
26+
config_dict = {
27+
"version": 1.1,
28+
"kind": "operation",
29+
"mount": [
30+
{"from": "./src", "to": "/dst"},
31+
{"from": "./src2"},
32+
{"to": "/dst2"},
33+
],
34+
"component": {
35+
"run": {"kind": V1RunKind.JOB, "container": {"image": "test"}}
36+
},
37+
}
38+
op_config = V1Operation.read(values=config_dict)
39+
assert len(op_config.mount) == 3
40+
assert isinstance(op_config.mount[0], V1Mount)
41+
assert op_config.mount[0].path_from == "./src"
42+
assert op_config.mount[0].path_to == "/dst"
43+
assert op_config.mount[1].path_from == "./src2"
44+
assert op_config.mount[1].path_to is None
45+
assert op_config.mount[2].path_from is None
46+
assert op_config.mount[2].path_to == "/dst2"
47+
assert op_config.get_resolved_mount() == [
48+
V1Mount(path_from="./src", path_to="/dst"),
49+
V1Mount(path_from="./src2", path_to=None),
50+
V1Mount(path_from=None, path_to="/dst2"),
51+
]

0 commit comments

Comments
 (0)