Skip to content

Commit 4531a81

Browse files
committed
Wire sandbox plugin into K8s services
* Copy plx-exec and the sandbox bootstrap script through the init tools container, wrap sandbox service commands, inject the derived sandbox token, and expose port 9090 on the pod and service spec.
1 parent e8ed1f9 commit 4531a81

22 files changed

Lines changed: 469 additions & 2948 deletions

File tree

cli/polyaxon/_env_vars/keys.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
ENV_KEYS_DASKCLUSTER_ENABLED = "POLYAXON_DASKCLUSTER_ENABLED"
107107

108108
# Sandbox
109+
ENV_KEYS_SANDBOX_TOKEN = "POLYAXON_SANDBOX_TOKEN"
109110
ENV_KEYS_SANDBOX_PORT = "POLYAXON_SANDBOX_PORT"
110111
ENV_KEYS_SANDBOX_HOST = "POLYAXON_SANDBOX_HOST"
111112
ENV_KEYS_SANDBOX_DEBUG = "POLYAXON_SANDBOX_DEBUG"

cli/polyaxon/_flow/plugins/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,12 @@ class V1Plugins(BaseSchemaModel):
159159
This plugin enables the sandbox daemon for programmatic exec, filesystem,
160160
and PTY access from SDKs, agents, and UIs.
161161
162+
Sandbox is supported for service runs only. When enabled, Polyaxon wraps the
163+
main container command with `/opt/polyaxon/bin/bootstrap-sandbox.sh`. If no
164+
command is provided, `plx-exec` runs as PID 1. If a workload is needed, set
165+
an explicit `command`; args without command are not supported in v0. The
166+
user image must provide `/bin/sh`.
167+
162168
To enable this plugin:
163169
164170
```yaml

cli/polyaxon/_k8s/converter/base/init.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,13 +515,31 @@ def _get_auth_context_init_container(
515515
def _get_tools_init_container(
516516
cls,
517517
polyaxon_init: V1PolyaxonInitContainer,
518+
use_tmux: bool = True,
519+
use_sandbox: bool = False,
518520
) -> k8s_schemas.V1Container:
521+
if not use_tmux and not use_sandbox:
522+
raise PolyaxonConverterError("Init tools container requires a tool.")
523+
524+
copy_commands = []
525+
if use_tmux:
526+
copy_commands.append("cp /usr/bin/tmux /opt/polyaxon/bin/tmux")
527+
if use_sandbox:
528+
copy_commands += [
529+
"cp /usr/bin/plx-exec /opt/polyaxon/bin/plx-exec",
530+
"cp /usr/bin/bootstrap-sandbox.sh "
531+
"/opt/polyaxon/bin/bootstrap-sandbox.sh",
532+
]
533+
command = ["sh", "-c", " && ".join(copy_commands)]
534+
if use_tmux and not use_sandbox:
535+
command = ["cp", "/usr/bin/tmux", "/opt/polyaxon/bin/tmux"]
536+
519537
return cls._patch_container(
520538
container=k8s_schemas.V1Container(
521539
name=INIT_TOOLS_CONTAINER,
522540
image=polyaxon_init.get_image(),
523541
image_pull_policy=polyaxon_init.image_pull_policy,
524-
command=["cp", "/usr/bin/tmux", "/opt/polyaxon/bin/tmux"],
542+
command=command,
525543
resources=polyaxon_init.get_resources(),
526544
volume_mounts=[cls._get_tools_bin_context_mount(read_only=False)],
527545
)

cli/polyaxon/_k8s/converter/base/main.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,31 @@
33
from clipped.utils.lists import to_list
44

55
from polyaxon._connections import V1Connection, V1ConnectionResource
6+
from polyaxon._env_vars.keys import ENV_KEYS_SANDBOX_TOKEN
67
from polyaxon._flow import V1Init, V1Plugins
78
from polyaxon._k8s import k8s_schemas
89
from polyaxon._runner.converter import BaseConverter as _BaseConverter
10+
from polyaxon._sandbox.auth import derive_sandbox_token_from_env
11+
from polyaxon._sandbox.constants import SANDBOX_BOOTSTRAP_PATH, SANDBOX_PORT
912
from polyaxon.exceptions import PolyaxonConverterError
1013

1114

15+
def _has_container_port(ports, port: int) -> bool:
16+
for container_port in to_list(ports, check_none=True):
17+
value = getattr(container_port, "container_port", None)
18+
if value is None and isinstance(container_port, dict):
19+
value = container_port.get("containerPort") or container_port.get(
20+
"container_port"
21+
)
22+
if str(value) == str(port):
23+
return True
24+
return False
25+
26+
27+
def _has_port(ports, port: int) -> bool:
28+
return any(str(p) == str(port) for p in to_list(ports, check_none=True))
29+
30+
1231
class MainConverter(_BaseConverter):
1332
def _get_main_container(
1433
self,
@@ -33,6 +52,21 @@ def _get_main_container(
3352
if artifacts_store and not run_path:
3453
raise PolyaxonConverterError("Run path is required for main container.")
3554

55+
if plugins and plugins.sandbox:
56+
if not main_container:
57+
raise PolyaxonConverterError(
58+
"plugins.sandbox requires a main container."
59+
)
60+
if main_container.args and not main_container.command:
61+
raise PolyaxonConverterError(
62+
"plugins.sandbox does not support args without command."
63+
)
64+
user_argv = to_list(main_container.command, check_none=True) + to_list(
65+
main_container.args, check_none=True
66+
)
67+
main_container.command = [SANDBOX_BOOTSTRAP_PATH]
68+
main_container.args = user_argv
69+
3670
if artifacts_store and (
3771
not plugins.collect_artifacts or plugins.mount_artifacts_store
3872
):
@@ -59,6 +93,7 @@ def _get_main_container(
5993
use_docker_context=plugins.docker,
6094
use_shm_context=plugins.shm,
6195
use_tmux_context=plugins.tmux,
96+
use_sandbox_context=plugins.sandbox,
6297
run_path=run_path,
6398
)
6499
if plugins
@@ -82,16 +117,38 @@ def _get_main_container(
82117
secrets=requested_secrets,
83118
config_maps=requested_config_maps,
84119
)
120+
if plugins and plugins.sandbox:
121+
env.append(
122+
self._get_env_var(
123+
name=ENV_KEYS_SANDBOX_TOKEN,
124+
value=derive_sandbox_token_from_env(self.run_uuid),
125+
)
126+
)
85127
env += self._get_resources_env_vars(main_container.resources)
86128

87129
# Env from
88130
env_from = self._get_env_from_k8s_resources(
89131
secrets=requested_secrets, config_maps=requested_config_maps
90132
)
91133

134+
ports = list(to_list(ports, check_none=True))
135+
if plugins and plugins.sandbox:
136+
ports = [
137+
port
138+
for port in ports
139+
if not _has_container_port(main_container.ports, port)
140+
]
141+
if (
142+
plugins
143+
and plugins.sandbox
144+
and not _has_port(ports, SANDBOX_PORT)
145+
and not _has_container_port(main_container.ports, SANDBOX_PORT)
146+
):
147+
ports.append(SANDBOX_PORT)
148+
92149
ports = [
93150
k8s_schemas.V1ContainerPort(container_port=port)
94-
for port in to_list(ports, check_none=True)
151+
for port in ports
95152
]
96153

97154
return self._patch_container(

cli/polyaxon/_k8s/converter/base/mounts.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def _get_mounts(
9898
use_shm_context: bool,
9999
use_artifacts_context: bool,
100100
use_tmux_context: bool = False,
101+
use_sandbox_context: bool = False,
101102
run_path: Optional[str] = None,
102103
) -> List[k8s_schemas.V1VolumeMount]:
103104
mounts = []
@@ -113,7 +114,7 @@ def _get_mounts(
113114
mounts.append(cls._get_docker_context_mount())
114115
if use_shm_context:
115116
mounts.append(cls._get_shm_context_mount())
116-
if use_tmux_context:
117+
if use_tmux_context or use_sandbox_context:
117118
mounts.append(cls._get_tools_bin_context_mount(read_only=True))
118119

119120
return mounts

cli/polyaxon/_k8s/converter/converters/service.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
from typing import Dict, Iterable, Optional
22

3+
from clipped.utils.lists import to_list
4+
35
from polyaxon._connections import V1Connection, V1ConnectionResource
46
from polyaxon._flow import V1CompiledOperation, V1Plugins
57
from polyaxon._k8s.converter.base import BaseConverter
68
from polyaxon._k8s.converter.mixins import ServiceMixin
79
from polyaxon._k8s.custom_resources.service import get_service_custom_resource
10+
from polyaxon._sandbox.constants import SANDBOX_PORT
811

912

1013
class ServiceConverter(ServiceMixin, BaseConverter):
14+
@staticmethod
15+
def _get_service_ports(ports, plugins: V1Plugins):
16+
ports = list(to_list(ports, check_none=True))
17+
if plugins and plugins.sandbox and SANDBOX_PORT not in ports:
18+
ports.append(SANDBOX_PORT)
19+
return ports
20+
1121
def get_resource(
1222
self,
1323
compiled_operation: V1CompiledOperation,
@@ -23,6 +33,7 @@ def get_resource(
2333
config=compiled_operation.plugins, auth=default_auth
2434
)
2535
kv_env_vars = compiled_operation.get_env_io()
36+
ports = self._get_service_ports(service.ports, plugins)
2637
replica_spec = self.get_replica_resource(
2738
plugins=plugins,
2839
environment=service.environment,
@@ -37,7 +48,7 @@ def get_resource(
3748
config_maps=config_maps,
3849
kv_env_vars=kv_env_vars,
3950
default_sa=default_sa,
40-
ports=service.ports,
51+
ports=ports,
4152
)
4253
return get_service_custom_resource(
4354
namespace=self.namespace,
@@ -53,7 +64,7 @@ def get_resource(
5364
notifications=plugins.notifications,
5465
labels=replica_spec.labels,
5566
annotations=replica_spec.annotations,
56-
ports=service.ports,
67+
ports=ports,
5768
is_external=service.is_external,
5869
replicas=service.replicas,
5970
)

cli/polyaxon/_k8s/converter/pod/volumes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,6 @@ def add_volume_from_resource(resource: V1ConnectionResource, is_secret: bool):
114114
volumes.append(get_configs_context_volume())
115115
if plugins and plugins.docker:
116116
volumes.append(get_docker_context_volume())
117-
if plugins and plugins.tmux:
117+
if plugins and (plugins.tmux or plugins.sandbox):
118118
volumes.append(get_tools_bin_context_volume())
119119
return volumes

cli/polyaxon/_runner/converter/converter.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,7 @@ def _get_mounts(
501501
use_shm_context: bool,
502502
use_artifacts_context: bool,
503503
use_tmux_context: bool = False,
504+
use_sandbox_context: bool = False,
504505
run_path: Optional[str] = None,
505506
) -> List[VolumeMount]:
506507
raise NotImplementedError
@@ -681,6 +682,8 @@ def _get_tensorboard_init_container(
681682
def _get_tools_init_container(
682683
cls,
683684
polyaxon_init: "V1PolyaxonInitContainer",
685+
use_tmux: bool = True,
686+
use_sandbox: bool = False,
684687
) -> Container:
685688
raise NotImplementedError
686689

@@ -943,10 +946,14 @@ def get_init_containers(
943946
)
944947
)
945948

946-
# Add tmux binary
947-
if plugins and plugins.tmux:
949+
# Add tool binaries
950+
if plugins and (plugins.tmux or plugins.sandbox):
948951
containers.append(
949-
self._get_tools_init_container(polyaxon_init=self.polyaxon_init)
952+
self._get_tools_init_container(
953+
polyaxon_init=polyaxon_init,
954+
use_tmux=plugins.tmux,
955+
use_sandbox=plugins.sandbox,
956+
)
950957
)
951958

952959
# Add outputs

cli/polyaxon/_sandbox/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
SANDBOX_BOOTSTRAP_PATH = "/opt/polyaxon/bin/bootstrap-sandbox.sh"
2+
SANDBOX_PORT = 9090

0 commit comments

Comments
 (0)