Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.9k
execute-type param addition in GkeCodeExecutor#4111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Uh oh!
There was an error while loading. Please reload this page.
Closed
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
0b28736
Add initial implementation to take in executor_type in gke_code_execu…
SHRUTI6991 cf9a092
Fix extension name.
SHRUTI6991 d5351f3
Address gemini comments to fix the dependency.
SHRUTI6991 ac876c5
Address comments to add logging and validation.
SHRUTI6991 9d1fd94
Address comments to reflect changes the class comment block.
SHRUTI6991 d9247be
Correct format for the failed format checks.
SHRUTI6991 8c9dbd3
Merge branch 'main' into execute-type/param-addition
SHRUTI6991 f183e80
Correct comment block.
SHRUTI6991 72dbf2d
Merge branch 'main' into execute-type/param-addition
SHRUTI6991 8a2a844
Add stderr to raise errors from sandbox failures.
SHRUTI6991 b1ec403
Merge branch 'main' into execute-type/param-addition
SHRUTI6991 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -17,6 +17,7 @@ | ||
| import logging | ||
| import uuid | ||
| from agentic_sandbox import SandboxClient | ||
| import kubernetes as k8s | ||
| from kubernetes.watch import Watch | ||
| @@ -36,9 +37,19 @@ | ||
| class GkeCodeExecutor(BaseCodeExecutor): | ||
| """Executes Python code in a secure gVisor-sandboxed Pod on GKE. | ||
| This executor securely runs code by dynamically creating a Kubernetes Job for | ||
| each execution request. The user's code is mounted via a ConfigMap, and the | ||
| Pod is hardened with a strict security context and resource limits. | ||
| This executor supports two modes of execution: 'job' and 'sandbox'. | ||
| Job Mode (default): | ||
| Securely runs code by dynamically creating a Kubernetes Job for each execution | ||
| request. The user's code is mounted via a ConfigMap, and the Pod is hardened | ||
| with a strict security context and resource limits. | ||
| Sandbox Mode: | ||
| Executes code using the Agent Sandbox Client. This mode requires additional | ||
| infrastructure to be deployed in the cluster, specifically: | ||
| - Agent-sandbox controller | ||
| - Sandbox templates (e.g., python-sandbox-template) | ||
| - Sandbox router and gateway | ||
| Key Features: | ||
| - Sandboxed execution using the gVisor runtime. | ||
| @@ -70,6 +81,8 @@ class GkeCodeExecutor(BaseCodeExecutor): | ||
| namespace: str = "default" | ||
| image: str = "python:3.11-slim" | ||
| timeout_seconds: int = 300 | ||
| executor_type: str = "job" # "job" or "sandbox" | ||
| sandbox_gateway_name: str | None = None | ||
| cpu_requested: str = "200m" | ||
| mem_requested: str = "256Mi" | ||
| # The maximum CPU the container can use, in "millicores". 1000m is 1 full CPU core. | ||
| @@ -79,11 +92,17 @@ class GkeCodeExecutor(BaseCodeExecutor): | ||
| kubeconfig_path: str | None = None | ||
| kubeconfig_context: str | None = None | ||
| # Sandbox constants | ||
| sandbox_template: str | None = None | ||
| _batch_v1: k8s.client.BatchV1Api | ||
| _core_v1: k8s.client.CoreV1Api | ||
| def __init__( | ||
| self, | ||
| executor_type: str = "job", | ||
SHRUTI6991 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| sandbox_gateway_name: str | None = None, | ||
| sandbox_template: str = "python-sandbox-template", | ||
| kubeconfig_path: str | None = None, | ||
| kubeconfig_context: str | None = None, | ||
| **data, | ||
| @@ -96,9 +115,17 @@ def __init__( | ||
| 3. Automatically via the default local kubeconfig file (~/.kube/config). | ||
| """ | ||
| super().__init__(**data) | ||
| self.executor_type = executor_type | ||
| self.sandbox_gateway_name = sandbox_gateway_name | ||
| self.sandbox_template = sandbox_template | ||
| self.kubeconfig_path = kubeconfig_path | ||
| self.kubeconfig_context = kubeconfig_context | ||
| if executor_type not in ["job", "sandbox"]: | ||
| raise ValueError( | ||
| f"Invalid executor_type: '{executor_type}'. Must be 'job' or" | ||
| " 'sandbox'." | ||
| ) | ||
| if self.kubeconfig_path: | ||
| try: | ||
| logger.info(f"Using explicit kubeconfig from '{self.kubeconfig_path}'.") | ||
| @@ -136,10 +163,28 @@ def __init__( | ||
| self._batch_v1 = client.BatchV1Api() | ||
| self._core_v1 = client.CoreV1Api() | ||
| def execute_code( | ||
| self, | ||
| invocation_context: InvocationContext, | ||
| code_execution_input: CodeExecutionInput, | ||
| def _execute_in_sandbox(self, code: str) -> CodeExecutionResult: | ||
| """Executes code using Agent Sandbox Client.""" | ||
| try: | ||
| with SandboxClient( | ||
| template_name=self.sandbox_template, | ||
| gateway_name=self.sandbox_gateway_name, | ||
| namespace=self.namespace, | ||
| ) as sandbox: | ||
| # Execute the code as a python script | ||
| logger.debug("Executing code in sandbox:\n```\n%s\n```", code) | ||
| sandbox.write("script.py", code) | ||
| result = sandbox.run("python3 script.py") | ||
| return CodeExecutionResult(stdout=result.stdout, stderr=result.stderr) | ||
| except Exception as e: | ||
| logger.error("Sandbox execution failed", exc_info=True) | ||
| return CodeExecutionResult( | ||
| stderr=f"Sandbox execution failed: {str(e)}", | ||
| ) | ||
SHRUTI6991 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| def _execute_as_job( | ||
| self, code: str, invocation_context: InvocationContext | ||
| ) -> CodeExecutionResult: | ||
| """Orchestrates the secure execution of a code snippet on GKE.""" | ||
| job_name = f"adk-exec-{uuid.uuid4().hex[:10]}" | ||
| @@ -150,7 +195,7 @@ def execute_code( | ||
| # 1. Create a ConfigMap to mount LLM-generated code into the Pod. | ||
| # 2. Create a Job that runs the code from the ConfigMap. | ||
| # 3. Set the Job as the ConfigMap's owner for automatic cleanup. | ||
| self._create_code_configmap(configmap_name, code_execution_input.code) | ||
| self._create_code_configmap(configmap_name, code) | ||
| job_manifest = self._create_job_manifest( | ||
| job_name, configmap_name, invocation_context | ||
| ) | ||
| @@ -162,7 +207,7 @@ def execute_code( | ||
| logger.info( | ||
| f"Submitted Job '{job_name}' to namespace '{self.namespace}'." | ||
| ) | ||
| logger.debug("Executing code:\n```\n%s\n```", code_execution_input.code) | ||
| logger.debug("Executing code:\n```\n%s\n```", code) | ||
| return self._watch_job_completion(job_name) | ||
| except ApiException as e: | ||
| @@ -186,6 +231,19 @@ def execute_code( | ||
| stderr=f"An unexpected executor error occurred: {e}" | ||
| ) | ||
| def execute_code( | ||
| self, | ||
| invocation_context: InvocationContext, | ||
| code_execution_input: CodeExecutionInput, | ||
| ) -> CodeExecutionResult: | ||
| """Overrides the base method to route execution based on executor_type.""" | ||
| code = code_execution_input.code | ||
| if self.executor_type == "sandbox": | ||
| return self._execute_in_sandbox(code) | ||
| else: | ||
| # Fallback to existing GKE Job logic | ||
| return self._execute_as_job(code, invocation_context) | ||
| def _create_job_manifest( | ||
| self, | ||
| job_name: str, | ||
137 changes: 137 additions & 0 deletions
137 tests/unittests/code_executors/test_gke_code_executor.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -71,6 +71,7 @@ def test_init_defaults(self): | ||
| assert executor.timeout_seconds == 300 | ||
| assert executor.cpu_requested == "200m" | ||
| assert executor.mem_limit == "512Mi" | ||
| assert executor.executor_type == "job" | ||
| def test_init_with_overrides(self): | ||
| """Tests that class attributes can be overridden at instantiation.""" | ||
| @@ -79,11 +80,19 @@ def test_init_with_overrides(self): | ||
| image="custom-python:latest", | ||
| timeout_seconds=60, | ||
| cpu_limit="1000m", | ||
| executor_type="sandbox", | ||
| ) | ||
| assert executor.namespace == "test-ns" | ||
| assert executor.image == "custom-python:latest" | ||
| assert executor.timeout_seconds == 60 | ||
| assert executor.cpu_limit == "1000m" | ||
| assert executor.executor_type == "sandbox" | ||
| assert executor.sandbox_template == "python-sandbox-template" | ||
| def test_init_invalid_executor_type(self): | ||
| """Tests that init raises ValueError for invalid executor_type.""" | ||
| with pytest.raises(ValueError, match="Invalid executor_type"): | ||
| GkeCodeExecutor(executor_type="invalid_type") | ||
| @patch("google.adk.code_executors.gke_code_executor.Watch") | ||
| def test_execute_code_success( | ||
| @@ -225,3 +234,131 @@ def test_create_job_manifest_structure(self, mock_invocation_context): | ||
| assert sec_context.allow_privilege_escalation is False | ||
| assert sec_context.read_only_root_filesystem is True | ||
| assert sec_context.capabilities.drop == ["ALL"] | ||
SHRUTI6991 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| @patch("google.adk.code_executors.gke_code_executor.SandboxClient") | ||
| def test_execute_code_forks_to_sandbox( | ||
| self, | ||
| mock_sandbox_client, | ||
| mock_invocation_context, | ||
| mock_k8s_clients, | ||
| ): | ||
| """Tests that execute_code uses SandboxClient when executor_type='sandbox'.""" | ||
| # Setup Sandbox mock | ||
| mock_sandbox_instance = ( | ||
| mock_sandbox_client.return_value.__enter__.return_value | ||
| ) | ||
| mock_run_result = MagicMock() | ||
| mock_run_result.stdout = "sandbox stdout" | ||
| mock_run_result.stderr = None | ||
| mock_sandbox_instance.run.return_value = mock_run_result | ||
| # Instantiate with sandbox type | ||
| executor = GkeCodeExecutor(executor_type="sandbox") | ||
| code_input = CodeExecutionInput(code='print("sandbox")') | ||
| # Execute | ||
| result = executor.execute_code(mock_invocation_context, code_input) | ||
| # Assertions | ||
| assert result.stdout == "sandbox stdout" | ||
| # Verify SandboxClient was used | ||
| mock_sandbox_client.assert_called_once() | ||
| mock_sandbox_instance.run.assert_called_once() | ||
| # Verify Job path was NOT taken | ||
| mock_k8s_clients["batch_v1"].create_namespaced_job.assert_not_called() | ||
| @patch("google.adk.code_executors.gke_code_executor.SandboxClient") | ||
| def test_execute_code_sandbox_exception( | ||
| self, | ||
| mock_sandbox_client, | ||
| mock_invocation_context, | ||
| ): | ||
| """Tests handling of exceptions from SandboxClient.""" | ||
| # Setup Sandbox mock to raise exception | ||
| mock_sandbox_client.return_value.__enter__.side_effect = Exception( | ||
| "Connection failed" | ||
| ) | ||
| # Instantiate with sandbox type | ||
| executor = GkeCodeExecutor(executor_type="sandbox") | ||
| code_input = CodeExecutionInput(code='print("sandbox")') | ||
| # Execute | ||
| result = executor.execute_code(mock_invocation_context, code_input) | ||
| # Assertions | ||
| assert result.stdout == "" | ||
| assert "Sandbox execution failed: Connection failed" in result.stderr | ||
| @patch("google.adk.code_executors.gke_code_executor.SandboxClient") | ||
| @patch("google.adk.code_executors.gke_code_executor.Watch") | ||
| def test_execute_code_forks_to_job( | ||
| self, | ||
| mock_watch, | ||
| mock_sandbox_client, | ||
| mock_invocation_context, | ||
| mock_k8s_clients, | ||
| ): | ||
| """Tests that execute_code uses K8s Job when executor_type='job'.""" | ||
| # Setup K8s Job mocks (success path) | ||
| mock_job = MagicMock() | ||
| mock_job.status.succeeded = True | ||
| mock_watch.return_value.stream.return_value = [{"object": mock_job}] | ||
| mock_pod = MagicMock() | ||
| mock_pod.metadata.name = "pod-1" | ||
| mock_k8s_clients["core_v1"].list_namespaced_pod.return_value.items = [ | ||
| mock_pod | ||
| ] | ||
| mock_k8s_clients["core_v1"].read_namespaced_pod_log.return_value = ( | ||
| "job stdout" | ||
| ) | ||
| # Instantiate with job type | ||
| executor = GkeCodeExecutor(executor_type="job") | ||
| code_input = CodeExecutionInput(code='print("job")') | ||
| # Execute | ||
| result = executor.execute_code(mock_invocation_context, code_input) | ||
| # Assertions | ||
| assert result.stdout == "job stdout" | ||
| # Verify Job path WAS taken | ||
| mock_k8s_clients["batch_v1"].create_namespaced_job.assert_called_once() | ||
| # Verify SandboxClient was NOT used | ||
| mock_sandbox_client.assert_not_called() | ||
| @patch("google.adk.code_executors.gke_code_executor.SandboxClient") | ||
| def test_execute_in_sandbox_returns_stderr( | ||
| self, | ||
| mock_sandbox_client, | ||
| mock_invocation_context, | ||
| ): | ||
| """Tests that stderr from the sandbox run is propagated to the result.""" | ||
| # Setup Sandbox mock | ||
| mock_sandbox_instance = ( | ||
| mock_sandbox_client.return_value.__enter__.return_value | ||
| ) | ||
| mock_run_result = MagicMock() | ||
| mock_run_result.stdout = "" | ||
| mock_run_result.stderr = "oops\n" | ||
| mock_sandbox_instance.run.return_value = mock_run_result | ||
| # Instantiate with sandbox type | ||
| executor = GkeCodeExecutor(executor_type="sandbox") | ||
| code_input = CodeExecutionInput( | ||
| code="import sys; print('oops', file=sys.stderr)" | ||
| ) | ||
| # Execute | ||
| result = executor.execute_code(mock_invocation_context, code_input) | ||
| # Assertions | ||
| assert result.stdout == "" | ||
| assert result.stderr == "oops\n" | ||
| mock_sandbox_instance.write.assert_called_with("script.py", code_input.code) | ||
| mock_sandbox_instance.run.assert_called_with("python3 script.py") | ||
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a direct Git URL with a specific commit hash for a dependency can make dependency management and updates challenging. It ties the project to an immutable state of the external repository, which might not receive updates or security patches easily. Consider if there's a way to depend on a published package with version ranges, or if this is a temporary measure, document the long-term plan for this dependency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a plan to transition this to a versioned release instead of a direct Git dependency to a specific commit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we are going to publish the python client to PyPI. This is an interim solution.