Skip to content

Commit 63be20d

Browse files
committed
Add --namespace flag support to workloads
Allows specifying a target Kubernetes namespace for workload commands (create, list, delete, wait). This avoids hardcoding the 'default' namespace and enables running workloads in custom namespaces. Also updates setup_k8s_service_accounts to create service accounts and role bindings in the specified namespace.
1 parent 337e676 commit 63be20d

7 files changed

Lines changed: 49 additions & 42 deletions

File tree

src/xpk/commands/cluster_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ def construct_args(**kwargs: Any) -> Namespace:
8989
project='project',
9090
zone='us-central1-a',
9191
reservation='',
92+
namespace='',
9293
on_demand=False,
9394
tpu_type=None,
9495
device_type=None,

src/xpk/commands/workload.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ def workload_create(args) -> None:
393393
k8s_api_client = None
394394
if not is_dry_run():
395395
k8s_api_client = setup_k8s_env(args)
396-
setup_k8s_service_accounts()
396+
setup_k8s_service_accounts(args.namespace)
397397

398398
workload_exists = check_if_workload_exists(args)
399399

@@ -786,7 +786,8 @@ def workload_create(args) -> None:
786786
)
787787

788788
tmp = write_tmp_file(yml_string)
789-
command = f'kubectl apply -f {str(tmp)}'
789+
ns_arg = f'-n {args.namespace}' if args.namespace else ''
790+
command = f'kubectl apply -f {str(tmp)} {ns_arg}'
790791
return_code = run_command_with_updates(command, 'Creating Workload')
791792

792793
if return_code != 0:
@@ -835,7 +836,7 @@ def workload_create(args) -> None:
835836
" python -c 'import pathwaysutils; import jax; print(jax.devices())'"
836837
)
837838
pathways_proxy_link = (
838-
f'https://console.cloud.google.com/kubernetes/job/{get_cluster_location(args.project, args.cluster, args.zone)}/{args.cluster}/default/{args.workload}-proxy-0/details?project={args.project}'
839+
f'https://console.cloud.google.com/kubernetes/job/{get_cluster_location(args.project, args.cluster, args.zone)}/{args.cluster}/{args.namespace}/{args.workload}-proxy-0/details?project={args.project}'
839840
)
840841
xpk_print(
841842
'Follow the proxy here:'
@@ -850,15 +851,15 @@ def workload_create(args) -> None:
850851
xpk_print(
851852
'Follow your workload here:'
852853
# pylint: disable=line-too-long
853-
f' https://console.cloud.google.com/kubernetes/service/{get_cluster_location(args.project, args.cluster, args.zone)}/{args.cluster}/default/{args.workload}/details?project={args.project}'
854+
f' https://console.cloud.google.com/kubernetes/service/{get_cluster_location(args.project, args.cluster, args.zone)}/{args.cluster}/{args.namespace}/{args.workload}/details?project={args.project}'
854855
)
855856
duration_of_logs = 'P1D' # Past 1 Day
856857
log_filter = (
857858
'resource.type="k8s_container"\n'
858859
f'resource.labels.project_id="{args.project}"\n'
859860
f'resource.labels.location="{get_cluster_location(args.project, args.cluster, args.zone)}"\n'
860861
f'resource.labels.cluster_name="{args.cluster}"\n'
861-
'resource.labels.namespace_name="default"\n'
862+
f'resource.labels.namespace_name="{args.namespace}"\n'
862863
f'resource.labels.pod_name:"{args.workload}-slice-job-0-0-"\n'
863864
'severity>=DEFAULT'
864865
)
@@ -916,7 +917,8 @@ def delete_workloads(args, workloads: list[str]) -> int:
916917
task_names = []
917918
for workload in workloads:
918919
args.workload = workload
919-
command = f'kubectl delete jobset {workload} -n default'
920+
ns_arg = f'-n {args.namespace}' if args.namespace else '-n default'
921+
command = f'kubectl delete jobset {workload} {ns_arg}'
920922
task_name = f'WorkloadDelete-{workload}'
921923
commands.append(command)
922924
task_names.append(task_name)

src/xpk/core/cluster.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -428,27 +428,27 @@ def get_gpu_type_from_cluster(args) -> str:
428428
return ''
429429

430430

431-
def setup_k8s_service_accounts() -> None:
431+
def setup_k8s_service_accounts(namespace: str = 'default') -> None:
432432
"""
433433
Creates/sets up SAs and the roles for them
434434
"""
435435
default_sa = 'default'
436436

437-
create_xpk_k8s_service_account()
437+
create_xpk_k8s_service_account(namespace)
438438

439-
role_name = create_pod_reader_role()
440-
create_role_binding(default_sa, role_name)
441-
create_role_binding(XPK_SA, role_name)
439+
role_name = create_pod_reader_role(namespace)
440+
create_role_binding(default_sa, role_name, namespace)
441+
create_role_binding(XPK_SA, role_name, namespace)
442442

443443

444-
def create_xpk_k8s_service_account() -> None:
444+
def create_xpk_k8s_service_account(namespace: str = 'default') -> None:
445445
k8s_core_client = k8s_client.CoreV1Api()
446446
sa = k8s_client.V1ServiceAccount(
447447
metadata=k8s_client.V1ObjectMeta(name=XPK_SA)
448448
)
449449

450450
try:
451-
k8s_core_client.read_namespaced_service_account(XPK_SA, DEFAULT_NAMESPACE)
451+
k8s_core_client.read_namespaced_service_account(XPK_SA, namespace)
452452
xpk_print(
453453
f'Service account: {XPK_SA} already exists. Skipping its creation.'
454454
)
@@ -461,7 +461,7 @@ def create_xpk_k8s_service_account() -> None:
461461
xpk_print(f'Creating a new service account: {XPK_SA}')
462462
try:
463463
k8s_core_client.create_namespaced_service_account(
464-
DEFAULT_NAMESPACE, sa, pretty=True
464+
namespace, sa, pretty=True
465465
)
466466
xpk_print(f'Created a new service account: {XPK_SA} successfully')
467467
except ApiException as e:
@@ -474,15 +474,15 @@ def create_xpk_k8s_service_account() -> None:
474474
xpk_exit(1)
475475

476476

477-
def create_pod_reader_role() -> str:
477+
def create_pod_reader_role(namespace: str = 'default') -> str:
478478
"""
479479
Creates the 'pod-reader' Role in the default namespace.
480480
"""
481481
k8s_rbac_client = k8s_client.RbacAuthorizationV1Api()
482482
role_name = 'pod-reader'
483483

484484
try:
485-
k8s_rbac_client.read_namespaced_role(role_name, DEFAULT_NAMESPACE)
485+
k8s_rbac_client.read_namespaced_role(role_name, namespace)
486486
xpk_print(f'Role: {role_name} already exists. Skipping its creation.')
487487
return role_name
488488
except ApiException as e:
@@ -492,7 +492,7 @@ def create_pod_reader_role() -> str:
492492

493493
role = k8s_client.V1Role(
494494
metadata=k8s_client.V1ObjectMeta(
495-
name=role_name, namespace=DEFAULT_NAMESPACE
495+
name=role_name, namespace=namespace
496496
),
497497
rules=[
498498
k8s_client.V1PolicyRule(
@@ -510,10 +510,10 @@ def create_pod_reader_role() -> str:
510510

511511
xpk_print(
512512
f'Attempting to create Role: {role_name} in namespace:'
513-
f' {DEFAULT_NAMESPACE}'
513+
f' {namespace}'
514514
)
515515
try:
516-
k8s_rbac_client.create_namespaced_role(DEFAULT_NAMESPACE, role, pretty=True)
516+
k8s_rbac_client.create_namespaced_role(namespace, role, pretty=True)
517517
xpk_print(f'Successfully created Role: {role_name}')
518518
return role_name
519519
except ApiException as e:
@@ -525,7 +525,7 @@ def create_pod_reader_role() -> str:
525525
xpk_exit(1)
526526

527527

528-
def create_role_binding(sa: str, role_name: str) -> None:
528+
def create_role_binding(sa: str, role_name: str, namespace: str = 'default') -> None:
529529
"""
530530
Creates a RoleBinding to associate the Service Account
531531
with the Role in the default namespace.
@@ -536,7 +536,7 @@ def create_role_binding(sa: str, role_name: str) -> None:
536536

537537
try:
538538
k8s_rbac_client.read_namespaced_role_binding(
539-
role_binding_name, DEFAULT_NAMESPACE
539+
role_binding_name, namespace
540540
)
541541
xpk_print(
542542
f'RoleBinding: {role_binding_name} already exists. Skipping its'
@@ -550,11 +550,11 @@ def create_role_binding(sa: str, role_name: str) -> None:
550550

551551
role_binding = k8s_client.V1RoleBinding(
552552
metadata=k8s_client.V1ObjectMeta(
553-
name=role_binding_name, namespace=DEFAULT_NAMESPACE
553+
name=role_binding_name, namespace=namespace
554554
),
555555
subjects=[
556556
k8s_client.RbacV1Subject(
557-
kind='ServiceAccount', name=sa, namespace=DEFAULT_NAMESPACE
557+
kind='ServiceAccount', name=sa, namespace=namespace
558558
)
559559
],
560560
role_ref=k8s_client.V1RoleRef(
@@ -565,11 +565,11 @@ def create_role_binding(sa: str, role_name: str) -> None:
565565
xpk_print(
566566
f'Attempting to create RoleBinding: {role_binding_name} for Service'
567567
f' Account: {sa} to Role: {role_name} in namespace:'
568-
f' {DEFAULT_NAMESPACE}'
568+
f' {namespace}'
569569
)
570570
try:
571571
k8s_rbac_client.create_namespaced_role_binding(
572-
DEFAULT_NAMESPACE, role_binding, pretty=True
572+
namespace, role_binding, pretty=True
573573
)
574574
xpk_print(f'Successfully created RoleBinding: {role_binding_name} for {sa}')
575575
except ApiException as e:

src/xpk/core/pathways.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def get_pathways_unified_query_link(args) -> str:
106106
f'resource.labels.project_id="{args.project}"\n'
107107
f'resource.labels.location="{get_cluster_location(args.project, args.cluster, args.zone)}"\n'
108108
f'resource.labels.cluster_name="{args.cluster}"\n'
109+
f'resource.labels.namespace_name="{args.namespace}"\n'
109110
f'resource.labels.pod_name:"{args.workload}-"\n'
110111
'severity>=DEFAULT'
111112
)
@@ -143,7 +144,8 @@ def try_to_delete_pathwaysjob_first(args, workloads) -> bool:
143144
task_names = []
144145
for workload in workloads:
145146
args.workload = workload
146-
command = f'kubectl delete pathwaysjob {workload} -n default'
147+
ns_arg = f'-n {args.namespace}' if args.namespace else '-n default'
148+
command = f'kubectl delete pathwaysjob {workload} {ns_arg}'
147149
task_name = f'PathwaysWorkloadDelete-{workload}'
148150
commands.append(command)
149151
task_names.append(task_name)

src/xpk/core/workload.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,10 @@ def _parse_workload_item(item: dict[str, Any]) -> _WorkloadListRow:
240240
def _fetch_workloads(
241241
filter_by_status: _StatusFilter,
242242
filter_by_job: Optional[str] = None,
243+
namespace: str = 'default',
243244
) -> tuple[int, list[_WorkloadListRow]]:
244245
"""Fetches and parses the raw workload list from the cluster."""
245-
command = 'kubectl get workloads --ignore-not-found -o=json'
246+
command = f'kubectl get workloads -n {namespace} --ignore-not-found -o=json'
246247

247248
task = f'List Jobs with filter-by-status={filter_by_status.value}'
248249
if filter_by_job:
@@ -399,7 +400,7 @@ def get_workload_list(args: argparse.Namespace) -> tuple[int, str]:
399400
filter_by_job = getattr(args, 'filter_by_job', None)
400401
filter_by_status = _get_status_filter(args.filter_by_status)
401402

402-
return_code, raw_rows = _fetch_workloads(filter_by_status, filter_by_job)
403+
return_code, raw_rows = _fetch_workloads(filter_by_status, filter_by_job, args.namespace)
403404
if return_code != 0:
404405
return return_code, ''
405406

@@ -425,7 +426,7 @@ def check_if_workload_exists(args: argparse.Namespace) -> bool:
425426

426427
s = ','.join([key + ':' + value for key, value in columns.items()])
427428

428-
command = f"kubectl get workloads -o=custom-columns='{s}'"
429+
command = f"kubectl get workloads -n {args.namespace} -o=custom-columns='{s}'"
429430
return_code, return_msg = run_command_for_value(
430431
command, 'Check if Workload Already Exists'
431432
)
@@ -442,16 +443,17 @@ def check_if_workload_exists(args: argparse.Namespace) -> bool:
442443
return False
443444

444445

445-
def _get_jobset_status(workload_name: str) -> tuple[int, str]:
446+
def _get_jobset_status(workload_name: str, namespace: str = 'default') -> tuple[int, str]:
446447
"""Retrieves the current status of a given jobset workload.
447448
448449
Args:
449450
workload_name: The name of the workload to retrieve the status for.
451+
namespace: The Kubernetes namespace to retrieve the status from.
450452
451453
Returns:
452454
A tuple containing the return code of the command (0 for success) and the status string.
453455
"""
454-
status_cmd = f'kubectl get jobset {workload_name} -o json'
456+
status_cmd = f'kubectl get jobset {workload_name} -n {namespace} -o json'
455457
return_code, return_value = run_command_for_value(
456458
status_cmd, 'Get jobset status'
457459
)
@@ -497,7 +499,7 @@ def wait_for_job_completion(args: argparse.Namespace) -> int:
497499
return 1
498500

499501
# Get the full workload name
500-
get_workload_name_cmd = f'kubectl get workloads | grep jobset-{args.workload}'
502+
get_workload_name_cmd = f'kubectl get workloads -n {args.namespace} | grep jobset-{args.workload}'
501503
return_code, return_value = run_command_for_value(
502504
get_workload_name_cmd, 'Get full workload name'
503505
)
@@ -512,7 +514,7 @@ def wait_for_job_completion(args: argparse.Namespace) -> int:
512514
f'{timeout_val}s' if timeout_val != -1 else 'max timeout (1 week)'
513515
)
514516
wait_cmd = (
515-
'kubectl wait --for=condition=Finished'
517+
f'kubectl wait -n {args.namespace} --for=condition=Finished'
516518
f' workload {full_workload_name} --timeout={timeout_val}s'
517519
)
518520
return_code, return_value = run_command_for_value(
@@ -526,7 +528,7 @@ def wait_for_job_completion(args: argparse.Namespace) -> int:
526528
f'Timed out waiting for your workload after {timeout_msg}, see your'
527529
' workload here:'
528530
# pylint: disable=line-too-long
529-
f' https://console.cloud.google.com/kubernetes/service/{get_cluster_location(args.project, args.cluster, args.zone)}/{args.cluster}/default/{args.workload}/details?project={args.project}'
531+
f' https://console.cloud.google.com/kubernetes/service/{get_cluster_location(args.project, args.cluster, args.zone)}/{args.cluster}/{args.namespace}/{args.workload}/details?project={args.project}'
530532
)
531533
return 124
532534
else:
@@ -536,9 +538,9 @@ def wait_for_job_completion(args: argparse.Namespace) -> int:
536538
xpk_print(
537539
'Finished waiting for your workload, see your workload here:'
538540
# pylint: disable=line-too-long
539-
f' https://console.cloud.google.com/kubernetes/service/{get_cluster_location(args.project, args.cluster, args.zone)}/{args.cluster}/default/{args.workload}/details?project={args.project}'
541+
f' https://console.cloud.google.com/kubernetes/service/{get_cluster_location(args.project, args.cluster, args.zone)}/{args.cluster}/{args.namespace}/{args.workload}/details?project={args.project}'
540542
)
541-
return_code, return_value = _get_jobset_status(args.workload)
543+
return_code, return_value = _get_jobset_status(args.workload, args.namespace)
542544
if return_code != 0:
543545
return return_code
544546

src/xpk/parser/common.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ def add_shared_arguments(
108108
),
109109
required=required,
110110
)
111+
custom_parser_or_group.add_argument(
112+
'--namespace',
113+
type=str,
114+
default='',
115+
help='Kubernetes namespace to use. Defaults to active namespace.',
116+
)
111117
custom_parser_or_group.add_argument(
112118
'--dry-run',
113119
type=bool,

src/xpk/parser/info.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,6 @@ def set_info_parser(info_parser: argparse.ArgumentParser) -> None:
3636
required=True,
3737
)
3838

39-
info_optional_arguments.add_argument(
40-
'--namespace',
41-
type=str,
42-
default='',
43-
help='Namespace to which resources and queues belong',
44-
)
4539

4640
queues_flitering_group = (
4741
info_optional_arguments.add_mutually_exclusive_group()

0 commit comments

Comments
 (0)