Repository: skypilot-org/skypilot
Pull request #5696 (2025-05-23 05:13:41)

Update workspace configuration doc
<!-- Describe the changes in this PR -->

This PR updates the configuration doc to make the update of workspaces in helm deployment more clear.

<img width="845" alt="image" src="https://github.com/user-attachments/assets/e8ce60bb-c5f6-4313-9a98-234722dd034a" />


<!-- Describe the tests ran -->
<!-- Unit tests (tests/test_*.py) are part of GitHub CI; below are tests that launch on the cloud. -->

Tested (run the relevant ones):

- [x] Code formatting: install pre-commit (auto-check on commit) or `bash format.sh`
- [ ] Any manual or new tests for this PR (please specify below)
- [ ] All smoke tests: `/smoke-test` (CI) or `pytest tests/test_smoke.py` (local)
- [ ] Relevant individual tests: `/smoke-test -k test_name` (CI) or `pytest tests/test_smoke.py::test_name` (local)
- [ ] Backward compatibility: `/quicktest-core` (CI) or `pytest tests/smoke_tests/test_backward_compat.py` (local)

<!-- CI commands (/-prefixed) can only be triggered by repo members -->



Patch:
diff --git a/docs/source/admin/workspaces.rst b/docs/source/admin/workspaces.rst
index 17e8f9681..cc745c3c1 100644
--- a/docs/source/admin/workspaces.rst
+++ b/docs/source/admin/workspaces.rst
@@ -16,7 +16,7 @@ Example use cases:
 Configuration
 -------------
 
-Define workspaces in your :ref:`SkyPilot config file <config-yaml>` on the API server host, i.e., ``~/.sky/config.yaml``.
+Workspaces are defined in the :ref:`SkyPilot config file <config-yaml>` of API server, i.e., ``~/.sky/config.yaml`` on the API server host.
 
 For example, assume:
 
@@ -24,7 +24,7 @@ For example, assume:
 - SkyPilot API server is configured to use AWS and GCP.
 - You want team-a to use both infra, and team-b to use GCP only with a different project ID.
 
-The above is achieved by configuring the following:
+The above is achieved by configuring the following section in the config file:
 
 .. code-block:: yaml
 
@@ -42,25 +42,45 @@ The above is achieved by configuring the following:
          disabled: true
 
 
-Schema of the ``workspaces`` section:
+.. dropdown:: Schema of the ``workspaces`` section:
 
-.. code-block:: yaml
+   .. code-block:: yaml
 
-   workspaces:
-     # Workspace 'default' is created by SkyPilot and is used if no workspaces
-     # are defined. Admins can optionally override settings for this workspace.
-     default: {}
+      workspaces:
+        # Workspace 'default' is created by SkyPilot and is used if no workspaces
+        # are defined. Admins can optionally override settings for this workspace.
+        default: {}
 
-     <workspace name>:
-       <infra name>:  # aws, gcp, ...; ssh; k8s
-         disabled: false  # Disable an infra provider for this workspace (default: false)
+        <workspace name>:
+           <infra name>:  # aws, gcp, ...; ssh; k8s
+             disabled: false  # Disable an infra provider for this workspace (default: false)
 
-       # Currently, 'gcp' supports an additional field:
-       gcp:
-         disabled: false
-         project_id: GCP project ID
+           # Currently, 'gcp' supports an additional field:
+           gcp:
+             disabled: false
+             project_id: GCP project ID
+
+To apply the configuration, follow the following steps:
+
+.. tab-set::
+
+   .. tab-item:: Helm Deployment
+
+      If you used :ref:`Helm Deployment <sky-api-server-helm-deploy-command>` for your API server, include workspace configuration to in local SkyPilot config file and run:
+
+      .. code-block:: bash
+
+         # RELEASE_NAME and NAMESPACE are the same as the ones used in the helm deployment
+         helm upgrade --install $RELEASE_NAME skypilot/skypilot-nightly --devel \
+            --namespace $NAMESPACE \
+            --reuse-values \
+            --set-file apiService.config=/your/path/to/config.yaml
+      
+      To change workspace configuration, update the config file and run the same command again. The API server will reload the new configuration automatically with no downtime. For more details, refer to :ref:`Setting the SkyPilot config in Helm Deployment <sky-api-server-config>`
 
+   .. tab-item:: VM Deployment
 
+      If you used a :ref:`VM Deployment <sky-api-server-cloud-deploy>` for your API server or testing workspaces locally, edit the workspace configuration in the :ref:`SkyPilot config file <config-yaml>`, `~/.sky/config.yaml`. API server will automatically reload the configuration to apply the changes.
 
 Setting the active workspace
 ----------------------------
diff --git a/sky/cli.py b/sky/cli.py
index d2e41192c..18641cec0 100644
--- a/sky/cli.py
+++ b/sky/cli.py
@@ -212,6 +212,7 @@ def _get_glob_storages(storages: List[str]) -> List[str]:
     """Returns a list of storages that match the glob pattern."""
     glob_storages = []
     for storage_object in storages:
+        # TODO(zhwu): client side should not rely on global_user_state.
         glob_storage = global_user_state.get_glob_storage_name(storage_object)
         if not glob_storage:
             click.echo(f'Storage {storage_object} not found.')
@@ -1788,9 +1789,7 @@ def _show_enabled_infra(active_workspace: str, show_workspace: bool):
     title = (f'{colorama.Fore.CYAN}{colorama.Style.BRIGHT}Enabled Infra'
              f'{workspace_str}:'
              f'{colorama.Style.RESET_ALL} ')
-
-    enabled_clouds = global_user_state.get_cached_enabled_clouds(
-        clouds.CloudCapability.COMPUTE, workspace=active_workspace)
+    enabled_clouds = sdk.get(sdk.enabled_clouds())
     enabled_ssh_infras = []
     enabled_k8s_infras = []
     enabled_cloud_infras = []
@@ -3688,6 +3687,7 @@ def show_gpus(
             yield from total_table.get_string()
 
         ctx_name = 'SSH Node Pool' if is_ssh else 'Context'
+        ctx_column_title = 'NODE_POOL' if is_ssh else 'CONTEXT'
 
         # print individual infos.
         for (ctx, k8s_realtime_table) in k8s_realtime_infos:
@@ -3705,7 +3705,8 @@ def show_gpus(
         if show_node_info:
             yield '\n'
             yield _format_kubernetes_node_info_combined(all_nodes_info,
-                                                        identity)
+                                                        identity,
+                                                        ctx_column_title)
 
     def _possibly_show_k8s_like_realtime(
             is_ssh: bool = False
diff --git a/sky/client/sdk.py b/sky/client/sdk.py
index a2a6c8ea7..af5fcca7b 100644
--- a/sky/client/sdk.py
+++ b/sky/client/sdk.py
@@ -151,11 +151,11 @@ def enabled_clouds(workspace: Optional[str] = None) -> server_common.RequestId:
     Request Returns:
         A list of enabled clouds in string format.
     """
-    response = requests.post(f'{server_common.get_server_url()}/enabled_clouds',
-                             json=json.loads(
-                                 payloads.EnabledCloudsRequestBody(
-                                     workspace=workspace).model_dump_json()),
-                             cookies=server_common.get_api_cookie_jar())
+    if workspace is None:
+        workspace = skypilot_config.get_active_workspace()
+    response = requests.get(
+        f'{server_common.get_server_url()}/enabled_clouds?workspace={workspace}',
+        cookies=server_common.get_api_cookie_jar())
     return server_common.get_request_id(response)
 
 
@@ -237,7 +237,7 @@ def list_accelerator_counts(
             accelerator names mapped to a list of available counts. See usage
             in cli.py.
     """
-    body = payloads.ListAcceleratorsBody(
+    body = payloads.ListAcceleratorCountsBody(
         gpus_only=gpus_only,
         name_filter=name_filter,
         region_filter=region_filter,
diff --git a/sky/server/requests/payloads.py b/sky/server/requests/payloads.py
index 1af805b37..397f84608 100644
--- a/sky/server/requests/payloads.py
+++ b/sky/server/requests/payloads.py
@@ -88,6 +88,11 @@ class RequestBody(pydantic.BaseModel):
     using_remote_api_server: bool = False
     override_skypilot_config: Optional[Dict[str, Any]] = {}
 
+    # Allow extra fields in the request body, which is useful for backward
+    # compatibility, i.e., we can add new fields to the request body without
+    # breaking the existing old API server.
+    model_config = pydantic.ConfigDict(extra='allow')
+
     def __init__(self, **data):
         data['env_vars'] = data.get('env_vars', request_body_env_vars())
         usage_lib_entrypoint = usage_lib.messages.usage.entrypoint
@@ -528,6 +533,6 @@ class UploadZipFileResponse(pydantic.BaseModel):
     missing_chunks: Optional[List[str]] = None
 
 
-class EnabledCloudsRequestBody(RequestBody):
+class EnabledCloudsBody(RequestBody):
     """The request body for the enabled clouds endpoint."""
     workspace: Optional[str] = None
diff --git a/sky/server/server.py b/sky/server/server.py
index 34a993b39..7273b08d0 100644
--- a/sky/server/server.py
+++ b/sky/server/server.py
@@ -263,15 +263,14 @@ async def check(request: fastapi.Request,
     )
 
 
-@app.post('/enabled_clouds')
-async def enabled_clouds(
-        request: fastapi.Request,
-        enabled_clouds_body: payloads.EnabledCloudsRequestBody) -> None:
+@app.get('/enabled_clouds')
+async def enabled_clouds(request: fastapi.Request,
+                         workspace: Optional[str] = None) -> None:
     """Gets enabled clouds on the server."""
     executor.schedule_request(
         request_id=request.state.request_id,
         request_name='enabled_clouds',
-        request_body=enabled_clouds_body,
+        request_body=payloads.RequestBody(workspace=workspace),
         func=core.enabled_clouds,
         schedule_type=requests_lib.ScheduleType.SHORT,
     )


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 34d9c5f3b..c913fedbc 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -75,8 +75,12 @@ class TestWithNoCloudEnabled:
                                    ['--cloud', 'aws', '--region', 'us-west-1'])
         assert not result.exit_code
 
+        result = cli_runner.invoke(cli.show_gpus,
+                                   ['--infra', 'aws/us-west-1'])
+        assert not result.exit_code
+
         for cloud in CLOUDS_TO_TEST:
-            result = cli_runner.invoke(cli.show_gpus, ['--cloud', cloud])
+            result = cli_runner.invoke(cli.show_gpus, ['--infra', cloud])
             assert not result.exit_code
 
             result = cli_runner.invoke(cli.show_gpus,