Repository: skypilot-org/skypilot
Pull request #5243 (2025-04-16 16:12:56)

[Doc] Fix missing namespace in API server deployment doc
<!-- Describe the changes in this PR -->

<!-- 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`
- [x] 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/_static/custom.css b/docs/source/_static/custom.css
index aae9defea..31a66e150 100644
--- a/docs/source/_static/custom.css
+++ b/docs/source/_static/custom.css
@@ -21,6 +21,29 @@
     z-index: 10001 !important; /* Set to 10001 to avoid overlap with kapa */
 }
 
+/* Override the default PyData sphinx theme announcement styling */
+.bd-header-announcement {
+    background-color: var(--pst-color-surface) !important;
+    border: none !important;
+    padding: 4px !important;
+    min-height: 0 !important;
+    height: auto !important;
+}
+
+.bd-header-announcement__content {
+    background-color: var(--pst-color-surface) !important;
+    color: var(--pst-color-text-base) !important;
+    padding: 0 !important;
+}
+
+.bd-header-announcement__content > div {
+    padding: 4px 8px !important;
+    line-height: 1 !important;
+}
+
+.bd-header-announcement a {
+    color: var(--pst-color-link) !important;
+}
 
 html[data-theme="light"] {
   --pst-color-link: #176de8;
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 582abe1ac..f32cda89f 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -117,7 +117,7 @@ html_theme_options = {
         'icon': 'fab fa-github',
     }],
     'use_edit_page_button': True,
-    'announcement': None,
+    'announcement': '<div style="padding: 24px; text-align: center; font-size: 0.8rem; line-height: 1;">👋 Join us for the <b>SkyPilot AI Infra Meetup</b> in San Francisco on April 24! <a href="https://lu.ma/r8ht5829?utm_source=skydocs">Register here</a></div>',
     'secondary_sidebar_items': [
         'page-toc',
         'edit-this-page',
diff --git a/docs/source/reference/api-server/api-server-admin-deploy.rst b/docs/source/reference/api-server/api-server-admin-deploy.rst
index a393a116d..74fc337ce 100644
--- a/docs/source/reference/api-server/api-server-admin-deploy.rst
+++ b/docs/source/reference/api-server/api-server-admin-deploy.rst
@@ -63,7 +63,7 @@ Following tabs describe how to configure credentials for different clouds on the
 
             NAMESPACE=skypilot
             kubectl create secret generic kube-credentials \
-              -n $NAMESPACE \
+              --namespace $NAMESPACE \
               --from-file=config=~/.kube/config
 
 
@@ -71,7 +71,8 @@ Following tabs describe how to configure credentials for different clouds on the
 
         .. code-block:: bash
 
-            helm upgrade --install skypilot skypilot/skypilot-nightly --devel \
+            helm --install skypilot skypilot/skypilot-nightly --devel \
+              --namespace $NAMESPACE \
               --set kubernetesCredentials.useKubeconfig=true \
               --set kubernetesCredentials.kubeconfigSecretName=kube-credentials \
               --set kubernetesCredentials.useApiServerCluster=true
@@ -106,7 +107,7 @@ Following tabs describe how to configure credentials for different clouds on the
 
             NAMESPACE=skypilot
             kubectl create secret generic aws-credentials \
-              -n $NAMESPACE \
+              --namespace $NAMESPACE \
               --from-literal=aws_access_key_id=YOUR_ACCESS_KEY_ID \
               --from-literal=aws_secret_access_key=YOUR_SECRET_ACCESS_KEY
 
@@ -116,7 +117,7 @@ Following tabs describe how to configure credentials for different clouds on the
 
         .. code-block:: bash
 
-            helm upgrade --install skypilot skypilot/skypilot-nightly --devel --set awsCredentials.enabled=true
+            helm --namespace $NAMESPACE upgrade --install skypilot skypilot/skypilot-nightly --devel --set awsCredentials.enabled=true
 
     .. tab-item:: GCP
         :sync: gcp-creds-tab
@@ -129,7 +130,7 @@ Following tabs describe how to configure credentials for different clouds on the
 
             NAMESPACE=skypilot
             kubectl create secret generic gcp-credentials \
-              -n $NAMESPACE \
+              --namespace $NAMESPACE \
               --from-file=gcp-cred.json=YOUR_SERVICE_ACCOUNT_JSON_KEY.json
 
         When installing or upgrading the Helm chart, enable GCP credentials by setting ``gcpCredentials.enabled=true`` and ``gcpCredentials.projectId`` to your project ID:
@@ -137,6 +138,7 @@ Following tabs describe how to configure credentials for different clouds on the
         .. code-block:: bash
 
             helm upgrade --install skypilot skypilot/skypilot-nightly --devel \
+              --namespace $NAMESPACE
               --set gcpCredentials.enabled=true \
               --set gcpCredentials.projectId=YOUR_PROJECT_ID
 
diff --git a/docs/source/reference/api-server/api-server.rst b/docs/source/reference/api-server/api-server.rst
index 54e11f1c5..132f17200 100644
--- a/docs/source/reference/api-server/api-server.rst
+++ b/docs/source/reference/api-server/api-server.rst
@@ -114,3 +114,27 @@ To verify that the API server is working, run ``sky api info``:
    Deploying API Server <api-server-admin-deploy>
    Upgrading API Server <api-server-upgrade>
    Troubleshooting <api-server-troubleshooting>
+
+By default, each user connected to the API server will only see their own resources.
+
+
+To see other users' clusters and the job/serve controllers, use the ``-u`` flag.
+
+.. code-block:: console
+    :emphasize-lines: 5,7,14
+
+    $ sky status -u
+    Clusters
+    NAME                          USER        LAUNCHED      RESOURCES                         STATUS   AUTOSTOP  COMMAND
+    my-cluster-2                  my-user     2 hrs ago     1x GCP(n2-standard-8)             STOPPED  -         sky launch task-2.yaml
+    other-cluster                 other-user  1 week ago    1x AWS(m6i.16xlarge)              UP       -         sky launch --cloud aws...
+    my-cluster-1                  my-user     2 months ago  1x AWS(m6i.4xlarge)               STOPPED  -         sky launch task-1.yaml
+    sky-jobs-controller-7c3d4ff7  root        2 days ago    1x AWS(r6i.xlarge, disk_size=50)  STOPPED  10m       sky jobs launch --env PART...
+
+    $ sky jobs queue -u
+    Fetching managed job statuses...
+    Managed jobs
+    ID  TASK  NAME       USER        RESOURCES  SUBMITTED   TOT. DURATION  JOB DURATION  #RECOVERIES  STATUS
+    3   -     job-2      my-user     1x[CPU:2]  2 days ago  2m 10s         1m 14s        0            CANCELLED
+    2   -     other-job  other-user  1x[CPU:2]  2 days ago  11m 54s        10m 52s       0            CANCELLED
+    1   -     job-1      my-use      1x[CPU:2]  5 days ago  1m 7s          3s            0            SUCCEEDED
diff --git a/sky/client/sdk.py b/sky/client/sdk.py
index 6adca454a..bef9c2442 100644
--- a/sky/client/sdk.py
+++ b/sky/client/sdk.py
@@ -1815,12 +1815,12 @@ def api_login(endpoint: Optional[str] = None) -> None:
     config_path = pathlib.Path(
         skypilot_config.get_user_config_path()).expanduser()
     with filelock.FileLock(config_path.with_suffix('.lock')):
-        if not skypilot_config.loaded():
+        if not config_path.exists():
             config_path.touch()
             config = {'api_server': {'endpoint': endpoint}}
         else:
-            config = skypilot_config.set_nested(('api_server', 'endpoint'),
-                                                endpoint)
+            config = skypilot_config.get_user_config()
+            config.set_nested(('api_server', 'endpoint'), endpoint)
         common_utils.dump_yaml(str(config_path), config)
         click.secho(f'Logged in to SkyPilot API server at {endpoint}',
                     fg='green')
diff --git a/sky/skypilot_config.py b/sky/skypilot_config.py
index 736196ac1..9070f5f82 100644
--- a/sky/skypilot_config.py
+++ b/sky/skypilot_config.py
@@ -118,6 +118,64 @@ def get_user_config_path() -> str:
     return _USER_CONFIG_PATH
 
 
+def get_user_config() -> config_utils.Config:
+    """Returns the user config."""
+    # find the user config file
+    user_config_path = _get_config_file_path(ENV_VAR_USER_CONFIG)
+    if user_config_path:
+        logger.debug('using user config file specified by '
+                     f'{ENV_VAR_USER_CONFIG}: {user_config_path}')
+        user_config_path = os.path.expanduser(user_config_path)
+        if not os.path.exists(user_config_path):
+            with ux_utils.print_exception_no_traceback():
+                raise FileNotFoundError(
+                    'Config file specified by env var '
+                    f'{ENV_VAR_USER_CONFIG} ({user_config_path!r}) '
+                    'does not exist. Please double check the path or unset the '
+                    f'env var: unset {ENV_VAR_USER_CONFIG}')
+    else:
+        user_config_path = get_user_config_path()
+        logger.debug(f'using default user config file: {user_config_path}')
+        user_config_path = os.path.expanduser(user_config_path)
+
+    # load the user config file
+    if os.path.exists(user_config_path):
+        user_config = _parse_config_file(user_config_path)
+        _validate_config(user_config, user_config_path)
+    else:
+        user_config = config_utils.Config()
+    return user_config
+
+
+def _get_project_config() -> config_utils.Config:
+    # find the project config file
+    project_config_path = _get_config_file_path(ENV_VAR_PROJECT_CONFIG)
+    if project_config_path:
+        logger.debug('using project config file specified by '
+                     f'{ENV_VAR_PROJECT_CONFIG}: {project_config_path}')
+        project_config_path = os.path.expanduser(project_config_path)
+        if not os.path.exists(project_config_path):
+            with ux_utils.print_exception_no_traceback():
+                raise FileNotFoundError(
+                    'Config file specified by env var '
+                    f'{ENV_VAR_PROJECT_CONFIG} ({project_config_path!r}) '
+                    'does not exist. Please double check the path or unset the '
+                    f'env var: unset {ENV_VAR_PROJECT_CONFIG}')
+    else:
+        logger.debug(
+            f'using default project config file: {_PROJECT_CONFIG_PATH}')
+        project_config_path = _PROJECT_CONFIG_PATH
+        project_config_path = os.path.expanduser(project_config_path)
+
+    # load the project config file
+    if os.path.exists(project_config_path):
+        project_config = _parse_config_file(project_config_path)
+        _validate_config(project_config, project_config_path)
+    else:
+        project_config = config_utils.Config()
+    return project_config
+
+
 def get_nested(keys: Tuple[str, ...],
                default_value: Any,
                override_configs: Optional[Dict[str, Any]] = None) -> Any:
@@ -253,54 +311,12 @@ def _reload_config_hierarchical() -> None:
     # Reset the global variables, to avoid using stale values.
     _dict = config_utils.Config()
 
-    # find the user config file
-    user_config_path = _get_config_file_path(ENV_VAR_USER_CONFIG)
-    if user_config_path:
-        logger.debug('using user config file specified by '
-                     f'{ENV_VAR_USER_CONFIG}: {user_config_path}')
-        user_config_path = os.path.expanduser(user_config_path)
-        if not os.path.exists(user_config_path):
-            with ux_utils.print_exception_no_traceback():
-                raise FileNotFoundError(
-                    'Config file specified by env var '
-                    f'{ENV_VAR_USER_CONFIG} ({user_config_path!r}) '
-                    'does not exist. Please double check the path or unset the '
-                    f'env var: unset {ENV_VAR_USER_CONFIG}')
-    else:
-        user_config_path = get_user_config_path()
-        logger.debug(f'using default user config file: {user_config_path}')
-        user_config_path = os.path.expanduser(user_config_path)
-
-    overrides = []
-
-    # find the project config file
-    project_config_path = _get_config_file_path(ENV_VAR_PROJECT_CONFIG)
-    if project_config_path:
-        logger.debug('using project config file specified by '
-                     f'{ENV_VAR_PROJECT_CONFIG}: {project_config_path}')
-        project_config_path = os.path.expanduser(project_config_path)
-        if not os.path.exists(project_config_path):
-            with ux_utils.print_exception_no_traceback():
-                raise FileNotFoundError(
-                    'Config file specified by env var '
-                    f'{ENV_VAR_PROJECT_CONFIG} ({project_config_path!r}) '
-                    'does not exist. Please double check the path or unset the '
-                    f'env var: unset {ENV_VAR_PROJECT_CONFIG}')
-    else:
-        logger.debug(
-            f'using default project config file: {_PROJECT_CONFIG_PATH}')
-        project_config_path = _PROJECT_CONFIG_PATH
-        project_config_path = os.path.expanduser(project_config_path)
-
-    # load the user config file
-    if os.path.exists(user_config_path):
-        user_config = _parse_config_file(user_config_path)
-        _validate_config(user_config, user_config_path)
+    overrides: List[config_utils.Config] = []
+    user_config = get_user_config()
+    if user_config:
         overrides.append(user_config)
-
-    if os.path.exists(project_config_path):
-        project_config = _parse_config_file(project_config_path)
-        _validate_config(project_config, project_config_path)
+    project_config = _get_project_config()
+    if project_config:
         overrides.append(project_config)
 
     # layer the configs on top of each other based on priority
diff --git a/sky/utils/config_utils.py b/sky/utils/config_utils.py
index 6209e210e..55f38f81b 100644
--- a/sky/utils/config_utils.py
+++ b/sky/utils/config_utils.py
@@ -210,19 +210,19 @@ def merge_k8s_configs(
                 merge_k8s_configs(base_config[key][0], value[0],
                                   next_allowed_override_keys,
                                   next_disallowed_override_keys)
-            elif key in ['volumes', 'volumeMounts']:
-                # If the key is 'volumes' or 'volumeMounts', we search for
-                # item with the same name and merge it.
-                for new_volume in value:
-                    new_volume_name = new_volume.get('name')
-                    if new_volume_name is not None:
-                        destination_volume = next(
+            elif key in ['volumes', 'volumeMounts', 'initContainers']:
+                # If the key is 'volumes', 'volumeMounts', or 'initContainers',
+                # we search for item with the same name and merge it.
+                for override_item in value:
+                    override_item_name = override_item.get('name')
+                    if override_item_name is not None:
+                        existing_base_item = next(
                             (v for v in base_config[key]
-                             if v.get('name') == new_volume_name), None)
-                        if destination_volume is not None:
-                            merge_k8s_configs(destination_volume, new_volume)
+                             if v.get('name') == override_item_name), None)
+                        if existing_base_item is not None:
+                            merge_k8s_configs(existing_base_item, override_item)
                         else:
-                            base_config[key].append(new_volume)
+                            base_config[key].append(override_item)
             else:
                 base_config[key].extend(value)
         else:


Test patch:
diff --git a/tests/unit_tests/test_sky/utils/test_config_utils.py b/tests/unit_tests/test_sky/utils/test_config_utils.py
index afdcd922f..aeee1e735 100644
--- a/tests/unit_tests/test_sky/utils/test_config_utils.py
+++ b/tests/unit_tests/test_sky/utils/test_config_utils.py
@@ -99,6 +99,27 @@ def test_merge_k8s_configs_with_container_resources():
     assert container['resources']['requests'] == {'cpu': '0.5', 'memory': '1Gi'}
 
 
+def test_merge_k8s_configs_with_init_container_resources():
+    """Test merging Kubernetes configs with initContainer specifications."""
+    base_config = {
+        'initContainers': [{
+            'name': 'init-container',
+            'image': 'init-image:latest',
+        }]
+    }
+    override_config = {
+        'initContainers': [{
+            'name': 'init-container',
+            'image': 'override-image:latest',
+        }]
+    }
+
+    config_utils.merge_k8s_configs(base_config, override_config)
+    assert len(base_config['initContainers']) == 1
+    container = base_config['initContainers'][0]
+    assert container['image'] == 'override-image:latest'
+
+
 def test_merge_k8s_configs_with_deeper_override():
     base_config = {
         'containers': [{