Skip to content

Commit 36f1a1c

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 36f1a1c

18 files changed

Lines changed: 535 additions & 15 deletions

File tree

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ def _get_mounts(
156156
use_shm_context: bool,
157157
use_artifacts_context: bool,
158158
use_tmux_context: bool = False,
159+
use_sandbox_context: bool = False,
159160
run_path: Optional[str] = None,
160161
) -> List[docker_types.V1VolumeMount]:
161162
mounts = []
@@ -171,7 +172,7 @@ def _get_mounts(
171172
mounts.append(cls._get_docker_context_mount())
172173
if use_shm_context:
173174
mounts.append(cls._get_shm_context_mount())
174-
if use_tmux_context:
175+
if use_tmux_context or use_sandbox_context:
175176
mounts.append(cls._get_tools_bin_context_mount(read_only=True))
176177

177178
return mounts

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/containers.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ def _patch_container(
3535
container.volume_mounts = to_list(
3636
container.volume_mounts, check_none=True
3737
) + to_list(volume_mounts, check_none=True)
38-
container.ports = to_list(container.ports, check_none=True) + to_list(
39-
ports, check_none=True
38+
container.ports = cls._merge_container_ports(
39+
to_list(container.ports, check_none=True),
40+
to_list(ports, check_none=True),
4041
)
4142
container.resources = container.resources or resources
4243
container._image_pull_policy = container.image_pull_policy or image_pull_policy
@@ -48,6 +49,41 @@ def _patch_container(
4849

4950
return cls._sanitize_container(container)
5051

52+
@staticmethod
53+
def _normalize_port(port) -> Optional[k8s_schemas.V1ContainerPort]:
54+
if isinstance(port, k8s_schemas.V1ContainerPort):
55+
return port
56+
if isinstance(port, dict):
57+
value = port.get("containerPort")
58+
if value is None:
59+
value = port.get("container_port")
60+
if value is None:
61+
return None
62+
return k8s_schemas.V1ContainerPort(
63+
container_port=int(value),
64+
name=port.get("name"),
65+
host_ip=port.get("hostIP") or port.get("host_ip"),
66+
host_port=port.get("hostPort") or port.get("host_port"),
67+
protocol=port.get("protocol"),
68+
)
69+
if isinstance(port, int):
70+
return k8s_schemas.V1ContainerPort(container_port=port)
71+
return None
72+
73+
@classmethod
74+
def _merge_container_ports(
75+
cls, existing: List, incoming: List
76+
) -> List[k8s_schemas.V1ContainerPort]:
77+
merged: List[k8s_schemas.V1ContainerPort] = []
78+
seen: set = set()
79+
for port in list(existing) + list(incoming):
80+
normalized = cls._normalize_port(port)
81+
if normalized is None or normalized.container_port in seen:
82+
continue
83+
seen.add(normalized.container_port)
84+
merged.append(normalized)
85+
return merged
86+
5187
@staticmethod
5288
def _sanitize_container_env(
5389
env: List[k8s_schemas.V1EnvVar],

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 = False,
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: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
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

@@ -33,6 +36,21 @@ def _get_main_container(
3336
if artifacts_store and not run_path:
3437
raise PolyaxonConverterError("Run path is required for main container.")
3538

39+
if plugins and plugins.sandbox:
40+
if not main_container:
41+
raise PolyaxonConverterError(
42+
"plugins.sandbox requires a main container."
43+
)
44+
if main_container.args and not main_container.command:
45+
raise PolyaxonConverterError(
46+
"plugins.sandbox does not support args without command."
47+
)
48+
user_argv = to_list(main_container.command, check_none=True) + to_list(
49+
main_container.args, check_none=True
50+
)
51+
main_container.command = [SANDBOX_BOOTSTRAP_PATH]
52+
main_container.args = user_argv
53+
3654
if artifacts_store and (
3755
not plugins.collect_artifacts or plugins.mount_artifacts_store
3856
):
@@ -59,6 +77,7 @@ def _get_main_container(
5977
use_docker_context=plugins.docker,
6078
use_shm_context=plugins.shm,
6179
use_tmux_context=plugins.tmux,
80+
use_sandbox_context=plugins.sandbox,
6281
run_path=run_path,
6382
)
6483
if plugins
@@ -82,17 +101,25 @@ def _get_main_container(
82101
secrets=requested_secrets,
83102
config_maps=requested_config_maps,
84103
)
104+
if plugins and plugins.sandbox:
105+
env.append(
106+
self._get_env_var(
107+
name=ENV_KEYS_SANDBOX_TOKEN,
108+
value=derive_sandbox_token_from_env(self.run_uuid),
109+
)
110+
)
85111
env += self._get_resources_env_vars(main_container.resources)
86112

87113
# Env from
88114
env_from = self._get_env_from_k8s_resources(
89115
secrets=requested_secrets, config_maps=requested_config_maps
90116
)
91117

92-
ports = [
93-
k8s_schemas.V1ContainerPort(container_port=port)
94-
for port in to_list(ports, check_none=True)
95-
]
118+
ports = list(to_list(ports, check_none=True))
119+
if plugins and plugins.sandbox and SANDBOX_PORT not in ports:
120+
ports.append(SANDBOX_PORT)
121+
122+
ports = [k8s_schemas.V1ContainerPort(container_port=port) for port in ports]
96123

97124
return self._patch_container(
98125
container=main_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/_local_process/converter/base/mounts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def _get_mounts(
8080
use_shm_context: bool,
8181
use_artifacts_context: bool,
8282
use_tmux_context: bool = False,
83+
use_sandbox_context: bool = False,
8384
run_path: Optional[str] = None,
8485
) -> List:
8586
return []

0 commit comments

Comments
 (0)