Skip to content

Commit 858d476

Browse files
committed
agent_sandbox: single-source the sandbox scheduling target
The gVisor scheduling selector and taint were duplicated as literal strings across the benchmark config, the installer DaemonSet, and the sandbox template, with nothing keeping them in sync. Untangle scheduling from runtime identity: - Scheduling: select the sandbox nodepool via the pkb_nodepool label PKB already injects on every pool, and derive the pod toleration from a single taint constant. nodeSelector/tolerations are now injected in Python (like _configure_controller_manifest) instead of being hardcoded in the manifests. - Runtime identity: runtimeClassName stays runsc, used only for the RuntimeClass, containerd registration, and the pod runtimeClassName. PKB does not yet apply nodepool taints to nodes (that lands in a follow-up), so the canonical taint lives in a _SANDBOX_TAINT constant with a TODO to read it from the nodepool config once that wiring exists. The SandboxWarmPool is unchanged: it inherits scheduling from the SandboxTemplate podTemplate.
1 parent 886aa87 commit 858d476

5 files changed

Lines changed: 174 additions & 37 deletions

File tree

perfkitbenchmarker/data/agent_sandbox/gvisor-installer/daemonset.yaml

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
# data/agent_sandbox/gvisor-installer/install.sh before this DaemonSet is
77
# applied). The ConfigMap key is "install.sh", mounted at /scripts.
88
#
9-
# Targets nodes labelled sandbox.gke.io/runtime=runsc (the label the
10-
# benchmark applies to the sandbox node pool).
9+
# nodeSelector and tolerations are injected at apply time (see
10+
# _render_gvisor_daemonset in resources/kubernetes/k8s_agent_sandbox.py) so the
11+
# DaemonSet targets the sandbox node pool via the pkb_nodepool label.
1112
apiVersion: apps/v1
1213
kind: DaemonSet
1314
metadata:
@@ -25,13 +26,6 @@ spec:
2526
app.kubernetes.io/name: gvisor-installer
2627
spec:
2728
hostPID: true
28-
tolerations:
29-
- key: sandbox.gke.io/runtime
30-
operator: Equal
31-
value: runsc
32-
effect: NoSchedule
33-
nodeSelector:
34-
sandbox.gke.io/runtime: runsc
3529
initContainers:
3630
- name: install
3731
image: docker.io/library/ubuntu:24.04

perfkitbenchmarker/data/agent_sandbox/sandbox-template.yaml.j2

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,6 @@ spec:
1919
automountServiceAccountToken: false
2020
securityContext:
2121
runAsNonRoot: true
22-
nodeSelector:
23-
sandbox.gke.io/runtime: {{ runtime_class }}
24-
tolerations:
25-
- key: sandbox.gke.io/runtime
26-
operator: Equal
27-
value: {{ runtime_class }}
28-
effect: NoSchedule
2922
containers:
3023
- name: python-runtime
3124
image: {{ image }}

perfkitbenchmarker/linux_benchmarks/agent_sandbox_benchmark.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,6 @@
7878
AWS:
7979
machine_type: m8i.4xlarge
8080
zone: us-east-1a
81-
node_labels:
82-
sandbox.gke.io/runtime: runsc
8381
node_taints:
8482
- sandbox.gke.io/runtime=runsc:NoSchedule
8583
agent_sandbox:

perfkitbenchmarker/resources/kubernetes/k8s_agent_sandbox.py

Lines changed: 104 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@
7979

8080
_SANDBOX_NAME = 'agent-sandbox'
8181

82+
# Config key of the nodepool the sandbox workload runs on (matches the
83+
# `nodepools:` block in linux_benchmarks/agent_sandbox_benchmark.py).
84+
_SANDBOX_NODEPOOL = 'sandbox'
85+
86+
# Taint fencing the sandbox nodepool. PKB does not yet apply nodepool taints to
87+
# nodes (that wiring lands in PR #6741), so keep the canonical taint string here
88+
# and derive the pod toleration from it. Keep this in lockstep with the
89+
# `node_taints` entry in agent_sandbox_benchmark.py BENCHMARK_CONFIG.
90+
# TODO(#6741): read this from cluster.nodepools[_SANDBOX_NODEPOOL].node_taints
91+
# once NodepoolConfig carries node_taints, and delete this constant.
92+
_SANDBOX_TAINT = 'sandbox.gke.io/runtime=runsc:NoSchedule'
93+
8294

8395
def _crd_name(filename):
8496
"""Derives a CRD resource name from its manifest filename.
@@ -109,17 +121,88 @@ def _wait_warmpool_ready(warmpool_name, replicas, timeout=600):
109121
)
110122

111123

112-
def install_gvisor():
124+
def _taint_to_toleration(taint):
125+
"""Converts a 'key=value:Effect' or 'key:Effect' taint to a toleration dict.
126+
127+
Parsing mirrors the planned EKS taint parser (PR #6744) but emits a
128+
Kubernetes toleration so the node taint and the pod toleration stay symmetric.
129+
"""
130+
spec, sep_effect, effect = taint.rpartition(':')
131+
if not sep_effect:
132+
raise ValueError(
133+
f'Malformed taint, expected key[=value]:Effect: {taint!r}'
134+
)
135+
key, sep_val, value = spec.partition('=')
136+
toleration = {'key': key}
137+
if sep_val:
138+
toleration['operator'] = 'Equal'
139+
toleration['value'] = value
140+
else:
141+
toleration['operator'] = 'Exists'
142+
toleration['effect'] = effect
143+
return toleration
144+
145+
146+
def _sandbox_scheduling(nodepool_name):
147+
"""Returns (node_selector, tolerations) targeting the sandbox nodepool.
148+
149+
The selector rendezvous is the pkb_nodepool label PKB stamps on every node
150+
pool (GKE _AddNodeParamsToCmd, EKS _RenderNodeGroupJson), so there is no
151+
bespoke label to keep in sync. The toleration is derived from _SANDBOX_TAINT.
152+
"""
153+
node_selector = {'pkb_nodepool': nodepool_name}
154+
tolerations = [_taint_to_toleration(_SANDBOX_TAINT)]
155+
return node_selector, tolerations
156+
157+
158+
def _render_gvisor_daemonset(node_selector, tolerations):
159+
"""Loads the installer DaemonSet and injects the sandbox scheduling target."""
160+
with open(data.ResourcePath(_GVISOR_DAEMONSET)) as manifest_file:
161+
manifest = yaml.safe_load(manifest_file)
162+
pod_spec = manifest['spec']['template']['spec']
163+
pod_spec['nodeSelector'] = node_selector
164+
pod_spec['tolerations'] = tolerations
165+
return yaml.dump(manifest, default_flow_style=False)
166+
167+
168+
def _render_template_manifest(template_spec, node_selector, tolerations):
169+
"""Renders the SandboxTemplate and injects the sandbox scheduling target.
170+
171+
nodeSelector/tolerations are set in Python (not the .j2) so the scheduling
172+
target has a single source of truth. runtimeClassName stays in the template:
173+
it is runtime identity, not scheduling.
174+
"""
175+
labels = template_spec.labels or {'sandbox': 'python-sandbox-bench'}
176+
rendered = vm_util.ReadAndRenderJinja2Template(
177+
_TEMPLATE_MANIFEST,
178+
trim_spaces=False,
179+
name=_SANDBOX_NAME,
180+
runtime_class=template_spec.runtime_class,
181+
image=template_spec.image,
182+
cpu_request=template_spec.cpu_request,
183+
cpu_limit=template_spec.cpu_limit,
184+
memory_request=template_spec.memory_request,
185+
memory_limit=template_spec.memory_limit,
186+
labels=labels,
187+
)
188+
manifest = yaml.safe_load(rendered)
189+
pod_spec = manifest['spec']['podTemplate']['spec']
190+
pod_spec['nodeSelector'] = node_selector
191+
pod_spec['tolerations'] = tolerations
192+
return yaml.dump(manifest, default_flow_style=False)
193+
194+
195+
def install_gvisor(node_selector, tolerations):
113196
"""Installs gVisor onto cluster nodes via the installer DaemonSet.
114197
115198
Creates the gvisor-installer-script ConfigMap (from install.sh) in
116199
kube-system before applying the DaemonSet. The DaemonSet mounts that
117200
ConfigMap at /scripts; the init container runs /scripts/install.sh.
118-
The ConfigMap must exist before the DaemonSet pods schedule.
201+
The ConfigMap must exist before the DaemonSet pods schedule. The DaemonSet's
202+
nodeSelector/tolerations are injected so it lands on the sandbox nodepool.
119203
"""
120204
_create_installer_configmap()
121-
# Plain .yaml files: ApplyManifest with NO kwargs.
122-
kubernetes_commands.ApplyManifest(_GVISOR_DAEMONSET)
205+
_apply_yaml(_render_gvisor_daemonset(node_selector, tolerations))
123206
kubernetes_commands.ApplyManifest(_GVISOR_RUNTIMECLASS)
124207
kubernetes_commands.WaitForRollout(
125208
'daemonset/gvisor-installer', namespace='kube-system'
@@ -302,19 +385,10 @@ def install_controller(
302385
)
303386

304387

305-
def apply_template(template_spec):
388+
def apply_template(template_spec, node_selector, tolerations):
306389
"""Applies the SandboxTemplate rendered from the template spec."""
307-
labels = template_spec.labels or {'sandbox': 'python-sandbox-bench'}
308-
kubernetes_commands.ApplyManifest(
309-
_TEMPLATE_MANIFEST,
310-
name=_SANDBOX_NAME,
311-
runtime_class=template_spec.runtime_class,
312-
image=template_spec.image,
313-
cpu_request=template_spec.cpu_request,
314-
cpu_limit=template_spec.cpu_limit,
315-
memory_request=template_spec.memory_request,
316-
memory_limit=template_spec.memory_limit,
317-
labels=labels,
390+
_apply_yaml(
391+
_render_template_manifest(template_spec, node_selector, tolerations)
318392
)
319393

320394

@@ -392,7 +466,8 @@ def _Delete(self):
392466
pass
393467

394468
def _InstallGvisor(self):
395-
install_gvisor()
469+
node_selector, tolerations = self._SandboxScheduling()
470+
install_gvisor(node_selector, tolerations)
396471

397472
def _InstallCrdsAndRbac(self):
398473
install_crds_and_rbac(self.spec.manifest_ref)
@@ -405,7 +480,18 @@ def _InstallController(self):
405480
)
406481

407482
def _ApplyTemplate(self):
408-
apply_template(self.spec.sandbox_template)
483+
node_selector, tolerations = self._SandboxScheduling()
484+
apply_template(self.spec.sandbox_template, node_selector, tolerations)
485+
486+
def _SandboxScheduling(self):
487+
"""Resolves (node_selector, tolerations) from the sandbox nodepool."""
488+
nodepool = self.cluster.nodepools.get(_SANDBOX_NODEPOOL)
489+
if nodepool is None:
490+
raise ValueError(
491+
f'Agent sandbox requires a nodepool named {_SANDBOX_NODEPOOL!r}; '
492+
'add it to the benchmark container_cluster nodepools config.'
493+
)
494+
return _sandbox_scheduling(nodepool.name)
409495

410496
def _InstallWarmpool(self):
411497
install_warmpool(self.spec.sandbox_warmpool.replicas)

tests/resources/kubernetes/k8s_agent_sandbox_test.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,11 @@ def _Sandbox(self, **template_overrides):
136136
sandbox_warmpool={'replicas': 3},
137137
sandbox_template=template_overrides or {'runtime_class': 'runsc'},
138138
)
139-
return k8s_agent_sandbox.K8sAgentSandbox(sandbox_spec, mock.Mock())
139+
cluster = mock.Mock()
140+
nodepool = mock.Mock()
141+
nodepool.name = 'sandbox'
142+
cluster.nodepools = {'sandbox': nodepool}
143+
return k8s_agent_sandbox.K8sAgentSandbox(sandbox_spec, cluster)
140144

141145
@mock.patch.object(k8s_agent_sandbox, 'install_crds_and_rbac')
142146
@mock.patch.object(k8s_agent_sandbox, 'install_gvisor')
@@ -231,5 +235,67 @@ def testConfigBuildsK8sAgentSandbox(self):
231235
self.assertIsInstance(sandbox, k8s_agent_sandbox.K8sAgentSandbox)
232236

233237

238+
class SandboxSchedulingTest(pkb_common_test_case.PkbCommonTestCase):
239+
240+
def testTaintToTolerationWithValue(self):
241+
self.assertEqual(
242+
k8s_agent_sandbox._taint_to_toleration(
243+
'sandbox.gke.io/runtime=runsc:NoSchedule'),
244+
{
245+
'key': 'sandbox.gke.io/runtime',
246+
'operator': 'Equal',
247+
'value': 'runsc',
248+
'effect': 'NoSchedule',
249+
})
250+
251+
def testTaintToTolerationNoValue(self):
252+
self.assertEqual(
253+
k8s_agent_sandbox._taint_to_toleration('dedicated:NoSchedule'),
254+
{'key': 'dedicated', 'operator': 'Exists', 'effect': 'NoSchedule'})
255+
256+
def testTaintToTolerationMalformedRaises(self):
257+
with self.assertRaises(ValueError):
258+
k8s_agent_sandbox._taint_to_toleration('no-effect')
259+
260+
def testSandboxSchedulingSelectorAndToleration(self):
261+
node_selector, tolerations = k8s_agent_sandbox._sandbox_scheduling('sandbox')
262+
self.assertEqual(node_selector, {'pkb_nodepool': 'sandbox'})
263+
self.assertEqual(tolerations, [{
264+
'key': 'sandbox.gke.io/runtime',
265+
'operator': 'Equal',
266+
'value': 'runsc',
267+
'effect': 'NoSchedule',
268+
}])
269+
270+
def testRenderGvisorDaemonsetSchedulesOnPkbNodepool(self):
271+
node_selector, tolerations = k8s_agent_sandbox._sandbox_scheduling('sandbox')
272+
manifest = yaml.safe_load(
273+
k8s_agent_sandbox._render_gvisor_daemonset(node_selector, tolerations))
274+
pod_spec = manifest['spec']['template']['spec']
275+
self.assertEqual(pod_spec['nodeSelector'], {'pkb_nodepool': 'sandbox'})
276+
self.assertEqual(pod_spec['tolerations'], tolerations)
277+
278+
def testRenderTemplateManifestSchedulingAndRuntimeClass(self):
279+
template_spec = mock.Mock()
280+
template_spec.runtime_class = 'runsc'
281+
template_spec.image = 'img:latest'
282+
template_spec.cpu_request = '500m'
283+
template_spec.cpu_limit = '2'
284+
template_spec.memory_request = '256Mi'
285+
template_spec.memory_limit = '1Gi'
286+
template_spec.labels = {'sandbox': 'python-sandbox-bench'}
287+
node_selector, tolerations = k8s_agent_sandbox._sandbox_scheduling('sandbox')
288+
manifest = yaml.safe_load(
289+
k8s_agent_sandbox._render_template_manifest(
290+
template_spec, node_selector, tolerations))
291+
pod_spec = manifest['spec']['podTemplate']['spec']
292+
self.assertEqual(pod_spec['nodeSelector'], {'pkb_nodepool': 'sandbox'})
293+
self.assertEqual(pod_spec['tolerations'], tolerations)
294+
# runtimeClassName stays as runtime identity, not scheduling.
295+
self.assertEqual(pod_spec['runtimeClassName'], 'runsc')
296+
# The old runtime label is no longer used as a node selector.
297+
self.assertNotIn('sandbox.gke.io/runtime', pod_spec['nodeSelector'])
298+
299+
234300
if __name__ == '__main__':
235301
unittest.main()

0 commit comments

Comments
 (0)