Skip to content

Commit c0675c1

Browse files
authored
Merge branch 'kubernetes-sigs:main' into agent-sandboxes-point-in-time-metrics-12557458534412660193
2 parents 45c449c + 60f0578 commit c0675c1

File tree

20 files changed

+923
-75
lines changed

20 files changed

+923
-75
lines changed

clients/python/agentic-sandbox-client/OWNERS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ approvers:
55
- justinsb
66
- barney-s
77
- vicentefb
8-
- igooch
8+
- igooch
9+
- SHRUTI6991

clients/python/agentic-sandbox-client/k8s_agent_sandbox/gke_extensions/test_podsnapshot_client.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,23 @@
3535

3636
class TestPodSnapshotSandboxClient(unittest.TestCase):
3737

38-
@patch("kubernetes.config")
39-
def setUp(self, mock_config):
38+
def setUp(self):
4039
logger.info("Setting up TestPodSnapshotSandboxClient...")
41-
# Mock kubernetes config loading
42-
mock_config.load_incluster_config.side_effect = config.ConfigException(
43-
"Not in cluster"
40+
41+
self.load_incluster_config_patcher = patch(
42+
"kubernetes.config.load_incluster_config"
4443
)
45-
mock_config.load_kube_config.return_value = None
44+
self.load_kube_config_patcher = patch("kubernetes.config.load_kube_config")
45+
46+
self.mock_load_incluster = self.load_incluster_config_patcher.start()
47+
self.addCleanup(self.load_incluster_config_patcher.stop)
48+
49+
self.mock_load_kube = self.load_kube_config_patcher.start()
50+
self.addCleanup(self.load_kube_config_patcher.stop)
51+
52+
# Mock kubernetes config loading
53+
self.mock_load_incluster.side_effect = config.ConfigException("Not in cluster")
54+
self.mock_load_kube.return_value = None
4655

4756
# Create client without patching super, as it's tested separately
4857
with patch.object(
@@ -247,7 +256,10 @@ def test_snapshot_success(self, mock_watch_cls):
247256
body={
248257
"apiVersion": f"{PODSNAPSHOT_API_GROUP}/{PODSNAPSHOT_API_VERSION}",
249258
"kind": f"{PODSNAPSHOTMANUALTRIGGER_API_KIND}",
250-
"metadata": {"name": result.trigger_name, "namespace": self.client.namespace},
259+
"metadata": {
260+
"name": result.trigger_name,
261+
"namespace": self.client.namespace,
262+
},
251263
"spec": {"targetPod": self.client.pod_name},
252264
},
253265
)
@@ -312,7 +324,7 @@ def test_snapshot_processed_retry(self, mock_watch_cls):
312324

313325
result = self.client.snapshot("test-retry")
314326

315-
self.assertTrue(result.success,result.error_reason)
327+
self.assertTrue(result.success, result.error_reason)
316328
self.assertEqual(result.snapshot_uid, "snapshot-uid-retry")
317329
logging.info("Finished test_snapshot_processed_retry.")
318330

clients/python/agentic-sandbox-client/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,6 @@ tracing = [
4242
"opentelemetry-exporter-otlp~=1.39.0",
4343
"opentelemetry-instrumentation-requests~=0.60b0",
4444
]
45+
46+
[tool.pytest.ini_options]
47+
testpaths = ["k8s_agent_sandbox"]

dev/OWNERS

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# See the OWNERS docs at https://go.k8s.io/owners
2+
3+
approvers:
4+
- janetkuo
5+
- justinsb
6+
- barney-s
7+
- vicentefb
8+
- igooch
9+
- SHRUTI6991

dev/ci/periodics/test-load-test

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2026 The Kubernetes Authors.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import os
17+
import shutil
18+
import subprocess
19+
import tempfile
20+
import sys
21+
22+
# Add the repository root to sys.path to allow importing from dev.*
23+
_repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
24+
if _repo_root not in sys.path:
25+
sys.path.insert(0, _repo_root)
26+
27+
from dev.ci.shared.runner import TestRunner
28+
29+
def install_clusterloader2(repo_root):
30+
"""Installs clusterloader2 if not present."""
31+
bin_dir = os.path.join(repo_root, "bin")
32+
if not os.path.exists(bin_dir):
33+
os.makedirs(bin_dir)
34+
cl2_path = os.path.join(bin_dir, "clusterloader2")
35+
if os.path.exists(cl2_path):
36+
return cl2_path
37+
38+
print("Installing clusterloader2...")
39+
with tempfile.TemporaryDirectory() as tmpdirname:
40+
subprocess.check_call(["git", "clone", "--depth", "1", "https://github.com/kubernetes/perf-tests.git", tmpdirname])
41+
42+
# Build from inside clusterloader2 directory as per README so go.mod is found
43+
build_dir = os.path.join(tmpdirname, "clusterloader2")
44+
cmd = ["go", "build", "-o", cl2_path, "./cmd/clusterloader.go"]
45+
subprocess.check_call(cmd, cwd=build_dir)
46+
return cl2_path
47+
48+
class LoadTestRunner(TestRunner):
49+
def __init__(self):
50+
super().__init__("load-test", "Invokes load-test in kind cluster and outputs a junit report in the ARTIFACTS dir")
51+
self.parser.add_argument(
52+
"--replicas",
53+
dest="replicas",
54+
help="Number of replicas per namespace",
55+
type=int,
56+
default=5,
57+
)
58+
self.parser.add_argument(
59+
"--namespaces",
60+
dest="namespaces",
61+
help="Number of namespaces",
62+
type=int,
63+
default=1,
64+
)
65+
self.parser.add_argument(
66+
"--qps",
67+
dest="qps",
68+
help="QPS for creating objects",
69+
type=float,
70+
default=10,
71+
)
72+
self.parser.add_argument(
73+
"--namespace-prefix",
74+
dest="namespace_prefix",
75+
help="Prefix for namespaces",
76+
type=str,
77+
default="agent-sandbox",
78+
)
79+
80+
def setup_cluster(self, args):
81+
return super().setup_cluster(args, extra_push_images_args=["--controller-only"])
82+
83+
def run_tests(self, args):
84+
cl2_path = install_clusterloader2(self.repo_root)
85+
test_config = "agent-sandbox-load-test.yaml"
86+
kubeconfig = os.path.join(self.repo_root, "bin/KUBECONFIG")
87+
88+
# Create overrides file with CLI arguments
89+
with tempfile.NamedTemporaryFile(mode='w', delete=False) as overrides_file:
90+
overrides_file.write(f"CL2_REPLICAS: {args.replicas}\n")
91+
overrides_file.write(f"CL2_NAMESPACES: {args.namespaces}\n")
92+
overrides_file.write(f"CL2_QPS: {args.qps}\n")
93+
overrides_file.write(f"CL2_NAMESPACE_PREFIX: {args.namespace_prefix}\n")
94+
overrides_path = overrides_file.name
95+
96+
try:
97+
# Run clusterloader2 from the load-test directory so relative paths in config work
98+
report_dir = os.path.join(self.repo_root, "bin")
99+
cmd = [cl2_path, f"--testconfig={test_config}", f"--kubeconfig={kubeconfig}", f"--testoverrides={overrides_path}", "--provider=kind", "--v=2", f"--report-dir={report_dir}"]
100+
print(f"Running load test: {' '.join(cmd)}")
101+
result = subprocess.run(cmd, cwd=os.path.join(self.repo_root, "dev/load-test"))
102+
finally:
103+
if os.path.exists(overrides_path):
104+
os.remove(overrides_path)
105+
# Cleanup kubeconfig and cl2 path for fresh runs
106+
if os.path.exists(kubeconfig):
107+
os.remove(kubeconfig)
108+
if os.path.exists(cl2_path):
109+
os.remove(cl2_path)
110+
return result
111+
112+
def copy_artifacts(self):
113+
artifact_dir = os.getenv("ARTIFACTS")
114+
if artifact_dir:
115+
if os.path.exists(f"{self.repo_root}/bin/junit.xml"):
116+
shutil.copy(f"{self.repo_root}/bin/junit.xml", f"{artifact_dir}/junit_load_test.xml")
117+
118+
if __name__ == "__main__":
119+
runner = LoadTestRunner()
120+
runner.main()

dev/ci/presubmits/test-e2e

Lines changed: 30 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -14,64 +14,38 @@
1414
# limitations under the License.
1515

1616
import os
17-
import argparse
1817
import shutil
19-
import sys
2018
import subprocess
19+
import sys
2120

22-
from shared import utils
23-
24-
repo_root = utils.get_repo_root()
25-
if repo_root not in sys.path:
26-
sys.path.insert(0, repo_root)
27-
28-
from dev.tools.shared import utils as tools_utils
29-
30-
def main(args):
31-
""" invokes e2e tests in kind cluster and outputs a junit report in the ARTIFACTS dir
32-
33-
The ARTIFACTS environment variable is set by prow.
34-
"""
35-
image_tag = tools_utils.get_image_tag()
36-
result = subprocess.run([f"{repo_root}/dev/tools/create-kind-cluster", "e2e-test", "--recreate", "--kubeconfig", f"{repo_root}/bin/KUBECONFIG"])
37-
if result.returncode != 0:
38-
return result.returncode
39-
result = subprocess.run([f"{repo_root}/dev/tools/push-images", "--kind-cluster-name", "e2e-test", "--image-prefix", args.image_prefix, "--image-tag", image_tag])
40-
if result.returncode != 0:
41-
return result.returncode
42-
result = subprocess.run([f"{repo_root}/dev/tools/deploy-to-kube", "--image-prefix", args.image_prefix, "--image-tag", image_tag])
43-
if result.returncode != 0:
44-
return result.returncode
45-
result = subprocess.run([f"{repo_root}/dev/tools/deploy-cloud-provider"])
46-
if result.returncode != 0:
47-
return result.returncode
48-
result = subprocess.run([f"{repo_root}/dev/tools/test-e2e", "--image-prefix", args.image_prefix, "--suite", args.suite])
49-
50-
# Always create junit file whether tests pass or fail
51-
artifact_dir = os.getenv("ARTIFACTS")
52-
if artifact_dir:
53-
shutil.copy(f"{repo_root}/bin/e2e-go-junit.xml", f"{artifact_dir}/junit_go.xml")
54-
shutil.copy(f"{repo_root}/bin/e2e-python-sdk-junit.xml", f"{artifact_dir}/junit_python_sdk.xml")
55-
56-
return result.returncode
57-
21+
# Add the repository root to sys.path to allow importing from dev.*
22+
_repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
23+
if _repo_root not in sys.path:
24+
sys.path.insert(0, _repo_root)
25+
26+
from dev.ci.shared.runner import TestRunner
27+
28+
class E2ETestRunner(TestRunner):
29+
def __init__(self):
30+
super().__init__("e2e-test", "invokes e2e tests in kind cluster and outputs a junit report in the ARTIFACTS dir")
31+
self.parser.add_argument(
32+
"--suite",
33+
dest="suite",
34+
help="which suite of tests to run",
35+
type=str,
36+
choices=["all", "tests", "benchmarks"],
37+
default="all",
38+
)
39+
40+
def run_tests(self, args):
41+
return subprocess.run([f"{self.repo_root}/dev/tools/test-e2e", "--image-prefix", args.image_prefix, "--suite", args.suite])
42+
43+
def copy_artifacts(self):
44+
artifact_dir = os.getenv("ARTIFACTS")
45+
if artifact_dir:
46+
shutil.copy(f"{self.repo_root}/bin/e2e-go-junit.xml", f"{artifact_dir}/junit_go.xml")
47+
shutil.copy(f"{self.repo_root}/bin/e2e-python-sdk-junit.xml", f"{artifact_dir}/junit_python_sdk.xml")
5848

5949
if __name__ == "__main__":
60-
parser = argparse.ArgumentParser()
61-
parser.add_argument(
62-
"--image-prefix",
63-
dest="image_prefix",
64-
help="prefix for the image name. requires slash at the end if a path",
65-
type=str,
66-
default="kind.local/",
67-
)
68-
parser.add_argument(
69-
"--suite",
70-
dest="suite",
71-
help="which suite of tests to run",
72-
type=str,
73-
choices=["all", "tests", "benchmarks"],
74-
default="all",
75-
)
76-
args = parser.parse_args()
77-
sys.exit(main(args))
50+
runner = E2ETestRunner()
51+
runner.main()

dev/ci/shared/runner.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2026 The Kubernetes Authors.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import os
17+
import argparse
18+
import sys
19+
import subprocess
20+
21+
from dev.tools.shared import utils as tools_utils
22+
23+
class TestRunner:
24+
def __init__(self, name, description):
25+
self.name = name
26+
self.description = description
27+
self.repo_root = self._get_repo_root()
28+
if self.repo_root not in sys.path:
29+
sys.path.insert(0, self.repo_root)
30+
31+
self.parser = argparse.ArgumentParser(description=self.description)
32+
self.parser.add_argument(
33+
"--image-prefix",
34+
dest="image_prefix",
35+
help="prefix for the image name. requires slash at the end if a path",
36+
type=str,
37+
default="kind.local/",
38+
)
39+
40+
def _get_repo_root(self):
41+
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
42+
43+
def setup_cluster(self, args, extra_push_images_args=None):
44+
image_tag = tools_utils.get_image_tag()
45+
result = subprocess.run([f"{self.repo_root}/dev/tools/create-kind-cluster", self.name, "--recreate", "--kubeconfig", f"{self.repo_root}/bin/KUBECONFIG"])
46+
if result.returncode != 0:
47+
return result
48+
49+
push_images_cmd = [f"{self.repo_root}/dev/tools/push-images", "--kind-cluster-name", self.name, "--image-prefix", args.image_prefix, "--image-tag", image_tag]
50+
if extra_push_images_args:
51+
push_images_cmd.extend(extra_push_images_args)
52+
result = subprocess.run(push_images_cmd)
53+
if result.returncode != 0:
54+
return result
55+
56+
result = subprocess.run([f"{self.repo_root}/dev/tools/deploy-to-kube", "--image-prefix", args.image_prefix, "--image-tag", image_tag])
57+
if result.returncode != 0:
58+
return result
59+
60+
result = subprocess.run([f"{self.repo_root}/dev/tools/deploy-cloud-provider"])
61+
return result
62+
63+
def run_tests(self, args):
64+
raise NotImplementedError
65+
66+
def copy_artifacts(self):
67+
pass
68+
69+
def main(self):
70+
args = self.parser.parse_args()
71+
result = self.setup_cluster(args)
72+
if result.returncode != 0:
73+
sys.exit(result.returncode)
74+
75+
result = self.run_tests(args)
76+
self.copy_artifacts()
77+
if result:
78+
sys.exit(result.returncode)
79+
else:
80+
sys.exit(0)

dev/tools/test-e2e

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ def main(args):
131131
env = os.environ.copy()
132132
env["IMAGE_TAG"] = utils.get_image_tag()
133133
env["IMAGE_PREFIX"] = utils.get_image_prefix(args)
134+
env["PIP_EXTRA_INDEX_URL"] = os.environ.get("PIP_EXTRA_INDEX_URL", "https://pypi.org/simple/")
134135

135136
if not setup_python_sdk(repo_root, env):
136137
return 1

0 commit comments

Comments
 (0)