diff --git a/pyproject.toml b/pyproject.toml index 19a36365e..bcd24c26b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ dev = [ version = {attr = "xpk.core.config.__version__"} [tool.setuptools] -packages = ["xpk", "xpk.parser", "xpk.core", "xpk.commands", "xpk.api", "xpk.templates", "xpk.utils", "xpk.core.blueprint", "xpk.core.remote_state", "xpk.core.workload_decorators"] +packages = ["xpk", "xpk.parser", "xpk.core", "xpk.commands", "xpk.api", "xpk.args", "xpk.templates", "xpk.utils", "xpk.core.blueprint", "xpk.core.remote_state", "xpk.core.workload_decorators"] package-dir = {"" = "src"} package-data = {"xpk.api" = ["storage_crd.yaml"], "xpk.templates" = ["storage.yaml"]} diff --git a/src/xpk/args/__init__.py b/src/xpk/args/__init__.py new file mode 100644 index 000000000..e7c0b7144 --- /dev/null +++ b/src/xpk/args/__init__.py @@ -0,0 +1,15 @@ +""" +Copyright 2024 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" diff --git a/src/xpk/args/batch.py b/src/xpk/args/batch.py new file mode 100644 index 000000000..4bbfea1a2 --- /dev/null +++ b/src/xpk/args/batch.py @@ -0,0 +1,22 @@ +""" +Copyright 2024 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from .cluster import ClusterConfig +from .slurm import SlurmConfig + + +class BatchArgs(ClusterConfig, SlurmConfig): + script: str = None diff --git a/src/xpk/args/cluster.py b/src/xpk/args/cluster.py new file mode 100644 index 000000000..4ff3bac03 --- /dev/null +++ b/src/xpk/args/cluster.py @@ -0,0 +1,24 @@ +""" +Copyright 2025 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from .gcloud_context import GcloudConfig + + +class ClusterConfig(GcloudConfig): + """Class representing cluster config""" + + kind_cluster: bool = False + cluster: str = None diff --git a/src/xpk/args/common.py b/src/xpk/args/common.py new file mode 100644 index 000000000..ec71ab773 --- /dev/null +++ b/src/xpk/args/common.py @@ -0,0 +1,21 @@ +""" +Copyright 2025 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + + +class GlobalConfig: + """Class representing global args type""" + + dry_run: bool = False diff --git a/src/xpk/args/gcloud_context.py b/src/xpk/args/gcloud_context.py new file mode 100644 index 000000000..ce16c6600 --- /dev/null +++ b/src/xpk/args/gcloud_context.py @@ -0,0 +1,128 @@ +""" +Copyright 2025 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from typing import Optional + +from google.api_core.exceptions import PermissionDenied +from google.cloud import resourcemanager_v3 + +import subprocess +import sys +from ..utils.console import xpk_exit, xpk_print +from .common import GlobalConfig + + +def get_project(): + """Get GCE project from `gcloud config get project`. + + Returns: + The project name. + """ + completed_command = subprocess.run( + ['gcloud', 'config', 'get', 'project'], check=True, capture_output=True + ) + project_outputs = completed_command.stdout.decode().strip().split('\n') + if len(project_outputs) < 1 or project_outputs[-1] == '': + sys.exit( + 'You must specify the project in the project flag or set it with' + " 'gcloud config set project '" + ) + return project_outputs[ + -1 + ] # The project name lives on the last line of the output + + +def get_zone(): + """Get GCE zone from `gcloud config get compute/zone`. + + Returns: + The zone name. + """ + completed_command = subprocess.run( + ['gcloud', 'config', 'get', 'compute/zone'], + check=True, + capture_output=True, + ) + zone_outputs = completed_command.stdout.decode().strip().split('\n') + if len(zone_outputs) < 1 or zone_outputs[-1] == '': + sys.exit( + "You must specify the zone in the zone flag or set it with 'gcloud" + " config set compute/zone '" + ) + return zone_outputs[-1] # The zone name lives on the last line of the output + + +class GcloudConfig(GlobalConfig): + """Class representing gcloud project config""" + + gke_version: Optional[str] = None + + _zone: Optional[str] = None + _project: Optional[str] = None + _project_number: Optional[str] = None + + @property + def zone(self) -> str: + if self._zone is None: + self._zone = get_zone() + if self._project is None: + self._project = get_project() + xpk_print(f'Working on {self._project} and {self._zone}') + return str(self._zone) + + @zone.setter + def zone(self, value: str): + self._zone = value + + @property + def region(self): + return '-'.join(self.zone.split('-')[:2]) + + @property + def project(self) -> str: + if self._project is None: + self._project = get_project() + if self._zone is None: + self._zone = get_zone() + xpk_print(f'Working on {self._project} and {self._zone}') + return str(self._project) + + @project.setter + def project(self, value: str): + self._project = value + + @property + def project_number(self) -> str: + if self._project_number is None: + client = resourcemanager_v3.ProjectsClient() + request = resourcemanager_v3.GetProjectRequest() + request.name = f'projects/{self.project}' + try: + response = client.get_project(request=request) + except PermissionDenied as e: + xpk_print( + f"Couldn't translate project id: {self.project} to project number." + f' Error: {e}' + ) + xpk_exit(1) + parts = response.name.split('/', 1) + xpk_print(f'Project number for project: {self.project} is {parts[1]}') + self._project_number = str(parts[1]) + return str(self._project_number) + + @project_number.setter + def project_number(self, value: str): + self._project_number = value diff --git a/src/xpk/args/slurm.py b/src/xpk/args/slurm.py new file mode 100644 index 000000000..045b19dcc --- /dev/null +++ b/src/xpk/args/slurm.py @@ -0,0 +1,38 @@ +""" +Copyright 2025 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from typing import Optional + + +class SlurmConfig: + """Class representing slurm args type""" + + ignore_unknown_flags: bool = False + array: Optional[str] = None + cpus_per_task: Optional[str] = None + gpus_per_task: Optional[str] = None + mem: Optional[str] = None + mem_per_task: Optional[str] = None + mem_per_cpu: Optional[str] = None + mem_per_gpu: Optional[str] = None + nodes: Optional[int] = None + ntasks: Optional[int] = None + output: Optional[str] = None + error: Optional[str] = None + input: Optional[str] = None + job_name: Optional[str] = None + chdir: Optional[str] = None + time: Optional[str] = None diff --git a/src/xpk/args/storage.py b/src/xpk/args/storage.py new file mode 100644 index 000000000..4ff796f60 --- /dev/null +++ b/src/xpk/args/storage.py @@ -0,0 +1,85 @@ +""" +Copyright 2024 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from typing import Optional, Literal, TypeAlias +from .cluster import ClusterConfig + + +StorageType: TypeAlias = Literal[ + 'gcsfuse', 'gcpfilestore', 'parallelstore', 'pd' +] + +StorageAccessMode: TypeAlias = Literal[ + 'ReadWriteOnce', 'ReadOnlyMany', 'ReadWriteMany' +] + +FilestoreTier: TypeAlias = Literal[ + 'BASIC_HDD', 'BASIC_SSD', 'ZONAL', 'REGIONAL', 'ENTERPRISE' +] + + +class StorageAttachArgs(ClusterConfig): + """Class representing storage attach args type""" + + name: str = None + type: StorageType = None + auto_mount: bool = None + mount_point: str = None + readonly: bool = None + size: Optional[int] = None + bucket: Optional[str] = None + vol: Optional[str] = None + access_mode: StorageAccessMode = 'ReadWriteMany' + instance: Optional[str] = None + prefetch_metadata: bool = True + manifest: Optional[str] = None + mount_options: Optional[str] = 'implicit-dirs' + + +class StorageCreateArgs(ClusterConfig): + """Class representing storage create args type""" + + name: str = None + access_mode: StorageAccessMode = 'ReadWriteMany' + vol: str = 'default' + size: int = None + tier: FilestoreTier = 'BASIC_HDD' + type: Literal['gcpfilestore'] = 'gcpfilestore' + auto_mount: bool = None + mount_point: str = None + readonly: bool = None + instance: Optional[str] = None + manifest: Optional[str] = None + mount_options: Optional[str] = 'implicit-dirs' + + +class StorageDeleteArgs(ClusterConfig): + """Class representing storage delete args type""" + + name: str = None + force: Optional[bool] = False + + +class StorageDetachArgs(ClusterConfig): + """Class representing storage detach args type""" + + name: str = None + + +class StorageListArgs(ClusterConfig): + """Class representing storage list args type""" + + pass diff --git a/src/xpk/args/utils.py b/src/xpk/args/utils.py new file mode 100644 index 000000000..f277ae8ad --- /dev/null +++ b/src/xpk/args/utils.py @@ -0,0 +1,35 @@ +""" +Copyright 2024 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import inspect +from argparse import Namespace +from typing import Any + + +def apply_args(main_args: Namespace, annotation: Any) -> Any: + args = annotation() + + # getters and setters + for param in inspect.get_annotations(annotation): + if param in main_args: + setattr(args, param, getattr(main_args, param)) + + # parameters + for param, _ in inspect.getmembers(annotation): + if param in main_args: + setattr(args, param, getattr(main_args, param)) + + return args # pytype: disable=bad-return-type diff --git a/src/xpk/commands/batch.py b/src/xpk/commands/batch.py index 9d81a709f..a2749d06f 100644 --- a/src/xpk/commands/batch.py +++ b/src/xpk/commands/batch.py @@ -15,8 +15,8 @@ """ import re -from argparse import Namespace +from ..args.batch import BatchArgs from ..core.cluster import ( create_xpk_k8s_service_account, get_cluster_credentials, @@ -36,7 +36,7 @@ from .kjob_common import add_gpu_networking_annotations_to_command -def batch(args: Namespace) -> None: +def batch(args: BatchArgs) -> None: """Run batch task. This function runs passed script in non-blocking manner. Args: @@ -60,7 +60,7 @@ def batch(args: Namespace) -> None: submit_job(args) -def submit_job(args: Namespace) -> None: +def submit_job(args: BatchArgs) -> None: create_xpk_k8s_service_account() diff --git a/src/xpk/commands/storage.py b/src/xpk/commands/storage.py index 2b6579ea6..1edff061d 100644 --- a/src/xpk/commands/storage.py +++ b/src/xpk/commands/storage.py @@ -14,23 +14,27 @@ limitations under the License. """ -from argparse import Namespace - import yaml from kubernetes import client as k8s_client from kubernetes.client import ApiClient from kubernetes.client.rest import ApiException +from ..args.storage import ( + StorageAttachArgs, + StorageCreateArgs, + StorageDeleteArgs, + StorageDetachArgs, + StorageListArgs, +) from ..core import gcsfuse from ..core.cluster import ( DEFAULT_NAMESPACE, - add_zone_and_project, get_cluster_network, setup_k8s_env, - update_cluster_with_parallelstore_driver_if_necessary, - update_cluster_with_pd_driver_if_necessary, update_cluster_with_gcpfilestore_driver_if_necessary, update_cluster_with_gcsfuse_driver_if_necessary, + update_cluster_with_parallelstore_driver_if_necessary, + update_cluster_with_pd_driver_if_necessary, update_cluster_with_workload_identity_if_necessary, ) from ..core.filestore import FilestoreClient, get_storage_class_name @@ -41,9 +45,9 @@ create_volume_bundle_instance, ) from ..core.storage import ( + GCE_PD_TYPE, GCP_FILESTORE_TYPE, GCS_FUSE_TYPE, - GCE_PD_TYPE, PARALLELSTORE_TYPE, STORAGE_CRD_PLURAL, XPK_API_GROUP_NAME, @@ -58,8 +62,7 @@ from ..utils.kubectl import apply_kubectl_manifest -def storage_create(args: Namespace) -> None: - add_zone_and_project(args) +def storage_create(args: StorageCreateArgs) -> None: if args.type == GCP_FILESTORE_TYPE: if args.instance is None: args.instance = args.name @@ -103,8 +106,7 @@ def storage_create(args: Namespace) -> None: apply_kubectl_manifest(k8s_api_client, manifest) -def storage_delete(args: Namespace) -> None: - add_zone_and_project(args) +def storage_delete(args: StorageDeleteArgs) -> None: k8s_api_client = setup_k8s_env(args) storages = list_storages(k8s_api_client) filestore_client = FilestoreClient(args.zone, args.name, args.project) @@ -137,8 +139,7 @@ def storage_delete(args: Namespace) -> None: filestore_client.delete_filestore_instance() -def storage_attach(args: Namespace) -> None: - add_zone_and_project(args) +def storage_attach(args: StorageAttachArgs) -> None: manifest = [{}] if args.type == GCP_FILESTORE_TYPE: if args.instance is None: @@ -209,7 +210,7 @@ def storage_attach(args: Namespace) -> None: apply_kubectl_manifest(k8s_api_client, manifest) -def enable_csi_drivers_if_necessary(args: Namespace) -> None: +def enable_csi_drivers_if_necessary(args: StorageAttachArgs) -> None: if args.type == GCS_FUSE_TYPE: return_code = update_cluster_with_workload_identity_if_necessary(args) if return_code > 0: @@ -235,13 +236,13 @@ def enable_csi_drivers_if_necessary(args: Namespace) -> None: xpk_exit(return_code) -def storage_list(args: Namespace) -> None: +def storage_list(args: StorageListArgs) -> None: k8s_api_client = setup_k8s_env(args) storages = list_storages(k8s_api_client) print_storages_for_cluster(storages) -def storage_detach(args: Namespace) -> None: +def storage_detach(args: StorageDetachArgs) -> None: k8s_api_client = setup_k8s_env(args) storage = get_storage(k8s_api_client, args.name) delete_storage_resources(k8s_api_client, storage) diff --git a/src/xpk/core/cluster.py b/src/xpk/core/cluster.py index fb6338fb6..a32598b98 100644 --- a/src/xpk/core/cluster.py +++ b/src/xpk/core/cluster.py @@ -19,7 +19,6 @@ from kubernetes import client as k8s_client from kubernetes import config from kubernetes.client.exceptions import ApiException -from .resources import get_cluster_system_characteristics from ..utils.console import xpk_exit, xpk_print from .capacity import H100_DEVICE_TYPE @@ -28,8 +27,14 @@ run_command_with_updates, run_command_with_updates_retry, ) -from .gcloud_context import add_zone_and_project, get_gke_server_config, zone_to_region +from .gcloud_context import ( + add_zone_and_project, + get_gke_server_config, + zone_to_region, +) +from ..args.cluster import ClusterConfig from .nodepool import upgrade_gke_nodepools_version +from .resources import get_cluster_system_characteristics from .system_characteristics import SystemCharacteristics JOBSET_VERSION = 'v0.8.0' @@ -45,7 +50,7 @@ # TODO(vbarr): Remove this function when jobsets gets enabled by default on # GKE clusters. -def set_jobset_on_cluster(args) -> int: +def set_jobset_on_cluster(args: ClusterConfig) -> int: """Add jobset command on server side and ask user to verify it is created. Args: @@ -74,7 +79,7 @@ def set_jobset_on_cluster(args) -> int: return return_code -def set_pathways_job_on_cluster(args) -> int: +def set_pathways_job_on_cluster(args: ClusterConfig) -> int: """Add PathwaysJob command on server side and ask user to verify it is created. Args: @@ -103,7 +108,9 @@ def set_pathways_job_on_cluster(args) -> int: return return_code -def install_nccl_on_cluster(args, system: SystemCharacteristics) -> int: +def install_nccl_on_cluster( + args: ClusterConfig, system: SystemCharacteristics +) -> int: """Install NCCL plugin on the cluster. Args: @@ -144,7 +151,7 @@ def install_nccl_on_cluster(args, system: SystemCharacteristics) -> int: return 0 -def install_nri_on_cluster(args) -> int: +def install_nri_on_cluster(args: ClusterConfig) -> int: """Install NRI Device Injector on the cluster. Args: @@ -169,7 +176,7 @@ def install_nri_on_cluster(args) -> int: return 0 -def get_cluster_network(args) -> str: +def get_cluster_network(args: ClusterConfig) -> str: xpk_print("Getting cluster's VPC network...") cluster_network_cmd = ( 'gcloud container clusters describe' @@ -185,7 +192,9 @@ def get_cluster_network(args) -> str: return val.strip() -def update_cluster_with_gcpfilestore_driver_if_necessary(args) -> int: +def update_cluster_with_gcpfilestore_driver_if_necessary( + args: ClusterConfig, +) -> int: """Updates a GKE cluster to enable GCPFilestore CSI driver, if not enabled already. Args: args: user provided arguments for running the command. @@ -205,7 +214,9 @@ def update_cluster_with_gcpfilestore_driver_if_necessary(args) -> int: return 0 -def update_cluster_with_parallelstore_driver_if_necessary(args) -> int: +def update_cluster_with_parallelstore_driver_if_necessary( + args: ClusterConfig, +) -> int: """Updates a GKE cluster to enable Parallelstore CSI driver, if not enabled already. Args: args: user provided arguments for running the command. @@ -224,7 +235,7 @@ def update_cluster_with_parallelstore_driver_if_necessary(args) -> int: return 0 -def update_cluster_with_pd_driver_if_necessary(args) -> int: +def update_cluster_with_pd_driver_if_necessary(args: ClusterConfig) -> int: """Updates a GKE cluster to enable PersistentDisk CSI driver, if not enabled already. Args: args: user provided arguments for running the command. @@ -245,7 +256,7 @@ def update_cluster_with_pd_driver_if_necessary(args) -> int: return 0 -def is_driver_enabled_on_cluster(args, driver: str) -> bool: +def is_driver_enabled_on_cluster(args: ClusterConfig, driver: str) -> bool: """Checks if the CSI driver is enabled on the cluster. Args: args: user provided arguments for running the command. @@ -271,7 +282,7 @@ def is_driver_enabled_on_cluster(args, driver: str) -> bool: return False -def update_gke_cluster_with_addon(args, addon: str) -> int: +def update_gke_cluster_with_addon(args: ClusterConfig, addon: str) -> int: """Run the GKE cluster update command for existing cluster and enabling passed addon. Args: args: user provided arguments for running the command. @@ -295,7 +306,7 @@ def update_gke_cluster_with_addon(args, addon: str) -> int: return 0 -def get_all_clusters_programmatic(args) -> tuple[list[str], int]: +def get_all_clusters_programmatic(args: ClusterConfig) -> tuple[list[str], int]: """Gets all the clusters associated with the project / region. Args: @@ -319,6 +330,7 @@ def get_all_clusters_programmatic(args) -> tuple[list[str], int]: return raw_cluster_output.splitlines(), 0 +# TODO: remove this function when we move to only using Configs and not Namespaces, make it on-demand in GcloudConfig def project_id_to_project_number(project_id: str) -> str: client = resourcemanager_v3.ProjectsClient() request = resourcemanager_v3.GetProjectRequest() @@ -336,17 +348,18 @@ def project_id_to_project_number(project_id: str) -> str: return str(parts[1]) -def setup_k8s_env(args) -> k8s_client.ApiClient: +def setup_k8s_env(args: ClusterConfig) -> k8s_client.ApiClient: if not getattr(args, 'kind_cluster', False): add_zone_and_project(args) get_cluster_credentials(args) + # TODO: remove line below when we move to only using Configs and not Namespaces args.project_number = project_id_to_project_number(args.project) config.load_kube_config() return k8s_client.ApiClient() # pytype: disable=bad-return-type -def get_gpu_type_from_cluster(args) -> str: +def get_gpu_type_from_cluster(args: ClusterConfig) -> str: system = get_cluster_system_characteristics(args) if not system is None: return system.device_type @@ -371,7 +384,7 @@ def create_xpk_k8s_service_account() -> None: ) -def update_gke_cluster_with_clouddns(args) -> int: +def update_gke_cluster_with_clouddns(args: ClusterConfig) -> int: """Run the GKE cluster update command for existing clusters and enable CloudDNS. Args: @@ -399,7 +412,9 @@ def update_gke_cluster_with_clouddns(args) -> int: return 0 -def update_gke_cluster_with_workload_identity_enabled(args) -> int: +def update_gke_cluster_with_workload_identity_enabled( + args: ClusterConfig, +) -> int: """Run the GKE cluster update command for existing cluster and enable Workload Identity Federation. Args: args: user provided arguments for running the command. @@ -426,7 +441,7 @@ def update_gke_cluster_with_workload_identity_enabled(args) -> int: return 0 -def update_gke_cluster_with_gcsfuse_driver_enabled(args) -> int: +def update_gke_cluster_with_gcsfuse_driver_enabled(args: ClusterConfig) -> int: """Run the GKE cluster update command for existing cluster and enable GCSFuse CSI driver. Args: args: user provided arguments for running the command. @@ -452,7 +467,9 @@ def update_gke_cluster_with_gcsfuse_driver_enabled(args) -> int: return 0 -def upgrade_gke_control_plane_version(args, default_rapid_gke_version) -> int: +def upgrade_gke_control_plane_version( + args: ClusterConfig, default_rapid_gke_version: str +) -> int: """Upgrade GKE cluster's control plane version before updating nodepools to use CloudDNS. Args: @@ -485,7 +502,7 @@ def upgrade_gke_control_plane_version(args, default_rapid_gke_version) -> int: return 0 -def is_cluster_using_clouddns(args) -> bool: +def is_cluster_using_clouddns(args: ClusterConfig) -> bool: """Checks if cluster is using CloudDNS. Args: args: user provided arguments for running the command. @@ -509,7 +526,7 @@ def is_cluster_using_clouddns(args) -> bool: return False -def is_workload_identity_enabled_on_cluster(args) -> bool: +def is_workload_identity_enabled_on_cluster(args: ClusterConfig) -> bool: """Checks if Workload Identity Federation is enabled on the cluster. Args: args: user provided arguments for running the command. @@ -537,7 +554,7 @@ def is_workload_identity_enabled_on_cluster(args) -> bool: return False -def is_gcsfuse_driver_enabled_on_cluster(args) -> bool: +def is_gcsfuse_driver_enabled_on_cluster(args: ClusterConfig) -> bool: """Checks if GCSFuse CSI driver is enabled on the cluster. Args: args: user provided arguments for running the command. @@ -562,7 +579,7 @@ def is_gcsfuse_driver_enabled_on_cluster(args) -> bool: return False -def update_cluster_with_clouddns_if_necessary(args) -> int: +def update_cluster_with_clouddns_if_necessary(args: ClusterConfig) -> int: """Updates a GKE cluster to use CloudDNS, if not enabled already. Args: @@ -607,7 +624,9 @@ def update_cluster_with_clouddns_if_necessary(args) -> int: return 0 -def update_cluster_with_workload_identity_if_necessary(args) -> int: +def update_cluster_with_workload_identity_if_necessary( + args: ClusterConfig, +) -> int: """Updates a GKE cluster to enable Workload Identity Federation, if not enabled already. Args: args: user provided arguments for running the command. @@ -629,7 +648,7 @@ def update_cluster_with_workload_identity_if_necessary(args) -> int: return 0 -def update_cluster_with_gcsfuse_driver_if_necessary(args) -> int: +def update_cluster_with_gcsfuse_driver_if_necessary(args: ClusterConfig) -> int: """Updates a GKE cluster to enable GCSFuse CSI driver, if not enabled already. Args: args: user provided arguments for running the command. @@ -649,7 +668,7 @@ def update_cluster_with_gcsfuse_driver_if_necessary(args) -> int: return 0 -def get_cluster_credentials(args) -> None: +def get_cluster_credentials(args: ClusterConfig) -> None: """Run cluster configuration command to set the kubectl config. Args: diff --git a/src/xpk/core/commands.py b/src/xpk/core/commands.py index ad01e6c21..70adf95bb 100644 --- a/src/xpk/core/commands.py +++ b/src/xpk/core/commands.py @@ -18,14 +18,20 @@ import subprocess import sys import time -from argparse import Namespace -from ..utils.objects import chunks -from ..utils.file import make_tmp_files, write_tmp_file from ..utils.console import xpk_print +from ..utils.file import make_tmp_files, write_tmp_file +from ..utils.objects import chunks +from ..args.common import GlobalConfig -def run_commands(commands, jobname, per_command_name, batch=10, dry_run=False): +def run_commands( + commands: list[str], + jobname: str, + per_command_name: list[str], + batch=10, + dry_run=False, +): """Run commands in groups of `batch`. Args: @@ -65,7 +71,9 @@ def run_commands(commands, jobname, per_command_name, batch=10, dry_run=False): return max_return_code -def run_command_batch(commands, jobname, per_command_name, output_logs): +def run_command_batch( + commands: list[str], jobname: str, per_command_name, output_logs +): """Runs commands in parallel. Args: @@ -130,7 +138,12 @@ def run_command_batch(commands, jobname, per_command_name, output_logs): def run_command_with_updates_retry( - command, task, args, verbose=True, num_retry_attempts=5, wait_seconds=10 + command: str, + task: str, + args: GlobalConfig, + verbose=True, + num_retry_attempts=5, + wait_seconds=10, ) -> int: """Generic run commands function with updates and retry logic. @@ -161,7 +174,9 @@ def run_command_with_updates_retry( return return_code -def run_command_with_updates(command, task, global_args, verbose=True) -> int: +def run_command_with_updates( + command: str, task: str, global_args: GlobalConfig, verbose=True +) -> int: """Generic run commands function with updates. Args: @@ -221,9 +236,9 @@ def run_command_with_updates(command, task, global_args, verbose=True) -> int: def run_command_for_value( - command, - task, - global_args, + command: str, + task: str, + global_args: GlobalConfig, dry_run_return_val='0', print_timer=False, hide_error=False, @@ -302,7 +317,7 @@ def run_command_for_value( def run_command_with_full_controls( command: str, task: str, - global_args: Namespace, + global_args: GlobalConfig, instructions: str | None = None, ) -> int: """Run command in current shell with system out, in and error handles. Wait @@ -349,7 +364,7 @@ def run_command_with_full_controls( return return_code -def run_kubectl_apply(yml_string: str, task: str, args: Namespace) -> int: +def run_kubectl_apply(yml_string: str, task: str, args: GlobalConfig) -> int: tmp = write_tmp_file(yml_string) command = f'kubectl apply -f {str(tmp.file.name)}' err_code = run_command_with_updates(command, task, args) diff --git a/src/xpk/core/gcloud_context.py b/src/xpk/core/gcloud_context.py index c1e386b85..53ebffeeb 100644 --- a/src/xpk/core/gcloud_context.py +++ b/src/xpk/core/gcloud_context.py @@ -14,55 +14,15 @@ limitations under the License. """ -import subprocess -import sys from dataclasses import dataclass from ..utils.console import xpk_print from .commands import run_command_for_value +from ..args.gcloud_context import GcloudConfig, get_project, get_zone -def get_project(): - """Get GCE project from `gcloud config get project`. - - Returns: - The project name. - """ - completed_command = subprocess.run( - ['gcloud', 'config', 'get', 'project'], check=True, capture_output=True - ) - project_outputs = completed_command.stdout.decode().strip().split('\n') - if len(project_outputs) < 1 or project_outputs[-1] == '': - sys.exit( - 'You must specify the project in the project flag or set it with' - " 'gcloud config set project '" - ) - return project_outputs[ - -1 - ] # The project name lives on the last line of the output - - -def get_zone(): - """Get GCE zone from `gcloud config get compute/zone`. - - Returns: - The zone name. - """ - completed_command = subprocess.run( - ['gcloud', 'config', 'get', 'compute/zone'], - check=True, - capture_output=True, - ) - zone_outputs = completed_command.stdout.decode().strip().split('\n') - if len(zone_outputs) < 1 or zone_outputs[-1] == '': - sys.exit( - "You must specify the zone in the zone flag or set it with 'gcloud" - " config set compute/zone '" - ) - return zone_outputs[-1] # The zone name lives on the last line of the output - - -def add_zone_and_project(args): +# TODO: remove when we stop using Namespaces as args +def add_zone_and_project(args: GcloudConfig): """Obtains the zone and project names from gcloud configs if not defined. Args: @@ -75,7 +35,7 @@ def add_zone_and_project(args): xpk_print(f'Working on {args.project} and {args.zone}') -def zone_to_region(zone) -> str: +def zone_to_region(zone: str) -> str: """Helper function converts zone name to region name. Args: @@ -96,7 +56,9 @@ class GkeServerConfig: valid_versions: set[str] -def get_gke_server_config(args) -> tuple[int, GkeServerConfig | None]: +def get_gke_server_config( + args: GcloudConfig, +) -> tuple[int, GkeServerConfig | None]: """Determine the GKE versions supported by gcloud currently. Args: @@ -154,7 +116,7 @@ def get_gke_server_config(args) -> tuple[int, GkeServerConfig | None]: def get_gke_control_plane_version( - args, gke_server_config: GkeServerConfig + args: GcloudConfig, gke_server_config: GkeServerConfig ) -> tuple[int, str | None]: """Determine gke control plane version for cluster creation. diff --git a/src/xpk/core/kjob.py b/src/xpk/core/kjob.py index 1ffca3d6e..c57af4b28 100644 --- a/src/xpk/core/kjob.py +++ b/src/xpk/core/kjob.py @@ -14,7 +14,6 @@ limitations under the License. """ -from argparse import Namespace from enum import Enum import yaml @@ -22,6 +21,8 @@ from kubernetes.client import ApiClient from kubernetes.client.rest import ApiException +from ..args.cluster import ClusterConfig +from ..args.common import GlobalConfig from ..utils import templates from ..utils.console import xpk_exit, xpk_print from .capacity import H100_DEVICE_TYPE, H100_MEGA_DEVICE_TYPE, H200_DEVICE_TYPE @@ -169,7 +170,7 @@ class PodTemplateDefaults(Enum): default_interface_annotation = "networking.gke.io/default-interface=eth0" -def get_a4_pod_template_annotations(args) -> tuple[str, str]: +def get_a4_pod_template_annotations(args: ClusterConfig) -> tuple[str, str]: sub_networks = get_cluster_subnetworks(args) interfaces_key, interfaces_value = rdma_decorator.get_interfaces_entry( sub_networks @@ -181,7 +182,9 @@ def get_a4_pod_template_annotations(args) -> tuple[str, str]: ) -def get_a3ultra_pod_template_annotations(args: Namespace) -> tuple[str, str]: +def get_a3ultra_pod_template_annotations( + args: ClusterConfig, +) -> tuple[str, str]: sub_networks = get_cluster_subnetworks(args) interfaces_key, interfaces_value = rdma_decorator.get_interfaces_entry( sub_networks @@ -194,7 +197,7 @@ def get_a3ultra_pod_template_annotations(args: Namespace) -> tuple[str, str]: def get_a3mega_pod_template_annotations( - args: Namespace, + args: ClusterConfig, ) -> tuple[str, str, str]: """Adds or updates annotations in the Pod template.""" sub_networks = get_cluster_subnetworks(args) @@ -207,7 +210,7 @@ def get_a3mega_pod_template_annotations( return tcpxo, interfaces, default_interface_annotation -def verify_kjob_installed(args: Namespace) -> int: +def verify_kjob_installed(args: GlobalConfig) -> int: """Check if kjob is installed. If not provide user with proper communicate and exit. Args: args - user provided arguments. @@ -249,7 +252,7 @@ def get_pod_template_interactive_command() -> str: def create_app_profile_instance( - args: Namespace, volume_bundles: list[str] + args: GlobalConfig, volume_bundles: list[str] ) -> int: """Create new AppProfile instance on cluster with default settings. @@ -284,7 +287,7 @@ def decorate_job_template_with_gpu(yml_string: str, gpu_type: str) -> str: def create_job_template_instance( - args: Namespace, + args: GlobalConfig, system: SystemCharacteristics | None, service_account: str, ) -> int: @@ -337,7 +340,9 @@ def create_job_template_instance( ) -def create_pod_template_instance(args: Namespace, service_account: str) -> int: +def create_pod_template_instance( + args: GlobalConfig, service_account: str +) -> int: """Create new PodTemplate instance on cluster with default settings. Args: @@ -367,7 +372,7 @@ def create_pod_template_instance(args: Namespace, service_account: str) -> int: ) -def prepare_kjob(args: Namespace) -> int: +def prepare_kjob(args: ClusterConfig) -> int: system = get_cluster_system_characteristics(args) k8s_api_client = setup_k8s_env(args) @@ -390,7 +395,7 @@ def prepare_kjob(args: Namespace) -> int: return create_app_profile_instance(args, volume_bundles) -def apply_kjob_crds(args: Namespace) -> int: +def apply_kjob_crds(args: GlobalConfig) -> int: """Apply kjob CRDs on cluster. This function install kjob CRDs files from kjobctl printcrds. @@ -473,7 +478,7 @@ def create_volume_bundle_instance( xpk_exit(1) -def get_storage_annotations(args: Namespace) -> list[str]: +def get_storage_annotations(args: ClusterConfig) -> list[str]: annotations = [] k8s_api_client = setup_k8s_env(args) diff --git a/src/xpk/core/resources.py b/src/xpk/core/resources.py index a9bc6c015..f7ae675c6 100644 --- a/src/xpk/core/resources.py +++ b/src/xpk/core/resources.py @@ -16,6 +16,8 @@ from dataclasses import dataclass +from ..args.cluster import ClusterConfig +from ..args.common import GlobalConfig from ..utils.console import xpk_print from ..utils.file import write_tmp_file from .capacity import ( @@ -29,7 +31,11 @@ ) from .commands import run_command_for_value, run_commands from .config import XPK_CURRENT_VERSION -from .system_characteristics import AcceleratorType, get_system_characteristics_by_device_type, SystemCharacteristics +from .system_characteristics import ( + AcceleratorType, + SystemCharacteristics, + get_system_characteristics_by_device_type, +) CLUSTER_RESOURCES_CONFIGMAP = 'resources-configmap' CLUSTER_METADATA_CONFIGMAP = 'metadata-configmap' @@ -50,7 +56,9 @@ class AutoprovisioningConfig: maximum_chips: int -def get_cluster_configmap(args, configmap_name) -> dict[str, str] | None: +def get_cluster_configmap( + args: GlobalConfig, configmap_name: str +) -> dict[str, str] | None: """Run the Get GKE Cluster ConfigMap request. Args: @@ -188,7 +196,7 @@ def create_or_update_cluster_configmap(configmap_yml: dict) -> int: return 0 -def check_cluster_resources(args, system) -> tuple[bool, bool]: +def check_cluster_resources(args: ClusterConfig, system) -> tuple[bool, bool]: """Check if cluster has resources of a specified device_type/gke_accelerator. This check will be skipped if -<_CLUSTER_RESOURCES_CONFIGMAP> ConfigMap doesn't exist for the cluster. @@ -216,7 +224,9 @@ def check_cluster_resources(args, system) -> tuple[bool, bool]: return True, False -def get_cluster_system_characteristics(args) -> SystemCharacteristics | None: +def get_cluster_system_characteristics( + args: ClusterConfig, +) -> SystemCharacteristics | None: """Get systemCharcteristics based on the cluster resources configMap Args: args: user provided arguments for running the command. diff --git a/src/xpk/main.py b/src/xpk/main.py index a85856cbd..4380f2614 100644 --- a/src/xpk/main.py +++ b/src/xpk/main.py @@ -33,14 +33,15 @@ import argparse import sys - +import inspect from .parser.core import set_parser from .utils.console import xpk_print from .utils.validation import validate_dependencies +from .args.utils import apply_args + ################### Compatibility Check ################### # Check that the user runs the below version or greater. - major_version_supported = 3 minor_version_supported = 10 @@ -64,7 +65,22 @@ validate_dependencies() main_args = parser.parse_args() main_args.enable_ray_cluster = False -main_args.func(main_args) + +sig = inspect.signature(main_args.func) +if len(sig.parameters) != 1 or ( + 'args' not in sig.parameters and 'config' not in sig.parameters +): + raise RuntimeError('Invalid method signature') + +if 'args' in sig.parameters and sig.parameters['args'].annotation in [ + argparse.Namespace, + inspect.Parameter.empty, +]: + main_args.func(main_args) +else: + param = 'args' if 'args' in sig.parameters else 'config' + args = apply_args(main_args, sig.parameters[param].annotation) + main_args.func(args) def main() -> None: diff --git a/src/xpk/parser/batch.py b/src/xpk/parser/batch.py index 937383235..1758648aa 100644 --- a/src/xpk/parser/batch.py +++ b/src/xpk/parser/batch.py @@ -14,16 +14,18 @@ limitations under the License. """ +from argparse import ArgumentParser + +from ..commands.batch import batch from .common import ( - add_shared_arguments, - add_slurm_arguments, add_cluster_arguments, add_kind_cluster_arguments, + add_shared_arguments, + add_slurm_arguments, ) -from ..commands.batch import batch -def set_batch_parser(batch_parser): +def set_batch_parser(batch_parser: ArgumentParser): batch_required_arguments = batch_parser.add_argument_group( 'batch Built-in Arguments', 'Arguments required for `batch`.' ) diff --git a/src/xpk/parser/config.py b/src/xpk/parser/config.py index ab328177c..4ff27adbd 100644 --- a/src/xpk/parser/config.py +++ b/src/xpk/parser/config.py @@ -14,12 +14,14 @@ limitations under the License. """ +import argparse + from ..commands.config import get_config, set_config from ..core.config import DEFAULT_KEYS from .common import add_shared_arguments -def set_config_parsers(config_parser): +def set_config_parsers(config_parser: argparse.ArgumentParser): add_shared_arguments(config_parser) config_subcommands = config_parser.add_subparsers( diff --git a/src/xpk/parser/core.py b/src/xpk/parser/core.py index 0572de877..efd3ae98d 100644 --- a/src/xpk/parser/core.py +++ b/src/xpk/parser/core.py @@ -16,20 +16,19 @@ import argparse -from .config import set_config_parsers - from ..utils.console import xpk_print +from .batch import set_batch_parser from .cluster import set_cluster_parser +from .config import set_config_parsers +from .info import set_info_parser from .inspector import set_inspector_parser -from .storage import set_storage_parser -from .workload import set_workload_parsers -from .batch import set_batch_parser from .job import set_job_parser -from .info import set_info_parser from .kind import set_kind_parser +from .run import set_run_parser from .shell import set_shell_parser +from .storage import set_storage_parser from .version import set_version_parser -from .run import set_run_parser +from .workload import set_workload_parsers def set_parser(parser: argparse.ArgumentParser): diff --git a/src/xpk/parser/info.py b/src/xpk/parser/info.py index 0bf122a16..18c6d97e3 100644 --- a/src/xpk/parser/info.py +++ b/src/xpk/parser/info.py @@ -14,10 +14,11 @@ limitations under the License. """ +import argparse + from ..commands.info import info from .common import add_shared_arguments from .validators import name_type -import argparse def set_info_parser(info_parser: argparse.ArgumentParser) -> None: diff --git a/src/xpk/parser/inspector.py b/src/xpk/parser/inspector.py index 16a018cb2..da3ca175d 100644 --- a/src/xpk/parser/inspector.py +++ b/src/xpk/parser/inspector.py @@ -14,12 +14,14 @@ limitations under the License. """ +import argparse + from ..commands.inspector import inspector -from .validators import name_type from .common import add_shared_arguments +from .validators import name_type -def set_inspector_parser(inspector_parser): +def set_inspector_parser(inspector_parser: argparse.ArgumentParser): inspector_parser.add_subparsers( title='inspector subcommands', dest='xpk_inspector_subcommands', diff --git a/src/xpk/parser/job.py b/src/xpk/parser/job.py index 898f81983..2c43bbe69 100644 --- a/src/xpk/parser/job.py +++ b/src/xpk/parser/job.py @@ -15,8 +15,8 @@ """ import argparse -from ..commands.job import job_info, job_list, job_cancel +from ..commands.job import job_cancel, job_info, job_list from .common import add_shared_arguments from .validators import name_type diff --git a/src/xpk/parser/kind.py b/src/xpk/parser/kind.py index 13e916178..8dd845214 100644 --- a/src/xpk/parser/kind.py +++ b/src/xpk/parser/kind.py @@ -14,16 +14,14 @@ limitations under the License. """ -from ..commands.kind import ( - cluster_create, - cluster_delete, - cluster_list, -) +import argparse + +from ..commands.kind import cluster_create, cluster_delete, cluster_list from .common import add_global_arguments from .validators import name_type -def set_kind_parser(kind_parser): +def set_kind_parser(kind_parser: argparse.ArgumentParser): cluster_subcommands = kind_parser.add_subparsers( title='kind subcommands', dest='xpk_kind_subcommands', diff --git a/src/xpk/parser/run.py b/src/xpk/parser/run.py index f5298afaa..8ade7d429 100644 --- a/src/xpk/parser/run.py +++ b/src/xpk/parser/run.py @@ -14,16 +14,18 @@ limitations under the License. """ +import argparse + from ..commands.run import run from .common import ( - add_shared_arguments, - add_slurm_arguments, add_cluster_arguments, add_kind_cluster_arguments, + add_shared_arguments, + add_slurm_arguments, ) -def set_run_parser(run_parser): +def set_run_parser(run_parser: argparse.ArgumentParser): run_required_arguments = run_parser.add_argument_group( 'Required Arguments', 'Arguments required for `run`.' ) diff --git a/src/xpk/parser/storage.py b/src/xpk/parser/storage.py index 16c6bbe91..cb061ef43 100644 --- a/src/xpk/parser/storage.py +++ b/src/xpk/parser/storage.py @@ -14,7 +14,7 @@ limitations under the License. """ -import argparse +from argparse import ArgumentParser, BooleanOptionalAction from ..commands.storage import ( storage_attach, @@ -30,7 +30,7 @@ ) -def set_storage_parser(storage_parser: argparse.ArgumentParser) -> None: +def set_storage_parser(storage_parser: ArgumentParser) -> None: storage_subcommands = storage_parser.add_subparsers( title='storage subcommands', dest='xpk_storage_subcommands', @@ -39,22 +39,31 @@ def set_storage_parser(storage_parser: argparse.ArgumentParser) -> None: ' specific subcommands for more details.' ), ) - add_storage_attach_parser(storage_subcommands) - add_storage_list_parser(storage_subcommands) - add_storage_detach_parser(storage_subcommands) - add_storage_create_parser(storage_subcommands) - add_storage_delete_parser(storage_subcommands) + storage_attach_parser = storage_subcommands.add_parser( + 'attach', help='Attach XPK Storage.' + ) + storage_create_parser = storage_subcommands.add_parser( + 'create', help='Create XPK Storage.' + ) + storage_delete_parser = storage_subcommands.add_parser( + 'delete', help='Delete XPK Storage.' + ) + storage_detach_parser = storage_subcommands.add_parser( + 'detach', help='Detach XPK Storage.' + ) + storage_list_parser = storage_subcommands.add_parser( + 'list', help='List XPK Storages.' + ) -def add_storage_attach_parser( - storage_subcommands_parser: argparse.ArgumentParser, -) -> None: + set_storage_attach_parser(storage_attach_parser) + set_storage_create_parser(storage_create_parser) + set_storage_delete_parser(storage_delete_parser) + set_storage_detach_parser(storage_detach_parser) + set_storage_list_parser(storage_list_parser) - storage_attach_parser: argparse.ArgumentParser = ( - storage_subcommands_parser.add_parser( - 'attach', help='attach XPK Storage.' - ) - ) + +def set_storage_attach_parser(storage_attach_parser: ArgumentParser) -> None: storage_attach_parser.set_defaults(func=storage_attach) req_args = storage_attach_parser.add_argument_group( 'Required Arguments', @@ -116,7 +125,7 @@ def add_storage_attach_parser( ) gcsfuse_args.add_argument( '--prefetch-metadata', - action=argparse.BooleanOptionalAction, + action=BooleanOptionalAction, default=True, help=( '(optional) Enables metadata pre-population when' @@ -171,14 +180,7 @@ def add_storage_attach_parser( add_kind_cluster_arguments(opt_args) -def add_storage_create_parser( - storage_subcommands_parser: argparse.ArgumentParser, -) -> None: - storage_create_parser: argparse.ArgumentParser = ( - storage_subcommands_parser.add_parser( - 'create', help='create XPK Storage.' - ) - ) +def set_storage_create_parser(storage_create_parser: ArgumentParser) -> None: storage_create_parser.set_defaults(func=storage_create) req_args = storage_create_parser.add_argument_group( 'Required Arguments', @@ -272,12 +274,7 @@ def add_storage_create_parser( add_kind_cluster_arguments(opt_args) -def add_storage_list_parser( - storage_subcommands_parser: argparse.ArgumentParser, -): - storage_list_parser: argparse.ArgumentParser = ( - storage_subcommands_parser.add_parser('list', help='List XPK Storages.') - ) +def set_storage_list_parser(storage_list_parser: ArgumentParser): storage_list_parser.set_defaults(func=storage_list) add_shared_arguments(storage_list_parser) req_args = storage_list_parser.add_argument_group( @@ -290,14 +287,7 @@ def add_storage_list_parser( ) -def add_storage_detach_parser( - storage_subcommands_parser: argparse.ArgumentParser, -): - storage_detach_parser: argparse.ArgumentParser = ( - storage_subcommands_parser.add_parser( - 'detach', help='Detach XPK Storage.' - ) - ) +def set_storage_detach_parser(storage_detach_parser: ArgumentParser): storage_detach_parser.set_defaults(func=storage_detach) add_shared_arguments(storage_detach_parser) @@ -315,14 +305,7 @@ def add_storage_detach_parser( add_kind_cluster_arguments(opt_args) -def add_storage_delete_parser( - storage_subcommands_parser: argparse.ArgumentParser, -): - storage_delete_parser: argparse.ArgumentParser = ( - storage_subcommands_parser.add_parser( - 'delete', help='Delete XPK Storage.' - ) - ) +def set_storage_delete_parser(storage_delete_parser: ArgumentParser): storage_delete_parser.set_defaults(func=storage_delete) add_shared_arguments(storage_delete_parser) diff --git a/src/xpk/utils/validation.py b/src/xpk/utils/validation.py index 87c12befb..5e544b669 100644 --- a/src/xpk/utils/validation.py +++ b/src/xpk/utils/validation.py @@ -14,12 +14,12 @@ limitations under the License. """ -from ..core.commands import run_command_for_value -from .console import xpk_exit, xpk_print +from ..args.common import GlobalConfig from ..commands.config import xpk_cfg -from ..core.config import DEPENDENCIES_KEY from ..commands.version import get_xpk_version - +from ..core.commands import run_command_for_value +from ..core.config import DEPENDENCIES_KEY +from .console import xpk_exit, xpk_print validation_commands = { 'kubectl': { @@ -72,7 +72,7 @@ def validate_dependencies(): for name, check in validation_commands.items(): cmd, message = check['command'], check['message'] code, _ = run_command_for_value( - cmd, f'Validate {name} installation.', None + cmd, f'Validate {name} installation.', GlobalConfig() ) if code != 0: xpk_print(message)