diff --git a/docs/source/examples/docker-containers.rst b/docs/source/examples/docker-containers.rst index 4d83823b8f9..2c4db3dd3d1 100644 --- a/docs/source/examples/docker-containers.rst +++ b/docs/source/examples/docker-containers.rst @@ -237,3 +237,23 @@ We suggest setting the :code:`SKYPILOT_DOCKER_PASSWORD` environment variable thr $ export SKYPILOT_DOCKER_PASSWORD=$(aws ecr get-login-password --region us-east-1) $ # Pass --env: $ sky launch task.yaml --env SKYPILOT_DOCKER_PASSWORD + +You can also directly specify shell commands in any Docker login environment variables in your YAML file, which will be executed to get the actual value: + +.. code-block:: yaml + + envs: + SKYPILOT_DOCKER_USERNAME: AWS + SKYPILOT_DOCKER_PASSWORD: "$(aws ecr get-login-password --region us-west-2)" + SKYPILOT_DOCKER_SERVER: .dkr.ecr.us-west-2.amazonaws.com + +SkyPilot will execute any value enclosed in :code:`$(...)` syntax and use the command's output as the actual value for the environment variable. This is especially useful for cloud provider registries where authentication tokens might need to be fetched dynamically. + +If you need to use a literal string that starts with :code:`$(` and ends with :code:`)` as your password or other Docker login value, you can escape it with a backslash: + +.. code-block:: yaml + + envs: + SKYPILOT_DOCKER_PASSWORD: "\\$(not-a-real-command)" + +This will be interpreted as the literal string :code:`$(not-a-real-command)` without attempting to execute it as a command. diff --git a/sky/provision/docker_utils.py b/sky/provision/docker_utils.py index 0aadcc55335..38070d11e35 100644 --- a/sky/provision/docker_utils.py +++ b/sky/provision/docker_utils.py @@ -4,6 +4,7 @@ import shlex import time from typing import Any, Dict, List +import subprocess from sky import sky_logging from sky.skylet import constants @@ -47,11 +48,70 @@ def format_image(self, image: str) -> str: @classmethod def from_env_vars(cls, d: Dict[str, str]) -> 'DockerLoginConfig': + username = d[constants.DOCKER_USERNAME_ENV_VAR] + password = d[constants.DOCKER_PASSWORD_ENV_VAR] + server = d[constants.DOCKER_SERVER_ENV_VAR] + + # Process command substitution in environment variables + username = cls._process_env_value(username, "username") + password = cls._process_env_value(password, "password") + server = cls._process_env_value(server, "server") + return cls( - username=d[constants.DOCKER_USERNAME_ENV_VAR], - password=d[constants.DOCKER_PASSWORD_ENV_VAR], - server=d[constants.DOCKER_SERVER_ENV_VAR], + username=username, + password=password, + server=server, ) + + @staticmethod + def _process_env_value(value: str, var_name: str) -> str: + """Process environment variable values, handling command substitution and escaping. + + Supports: + 1. Command execution with $(command) syntax + 2. Escaping with \\$(command) to use the literal string + + Args: + value: The environment variable value to process + var_name: Name of the variable (for error reporting) + + Returns: + Processed value with commands executed if applicable + """ + if not isinstance(value, str): + return value + + # Handle escaped command syntax: \$(command) -> $(command) + if value.startswith('\\$(') and value.endswith(')'): + return value[1:] # Remove the escape character + + # Handle command execution: $(command) -> execute and return result + if value.startswith('$(') and value.endswith(')'): + # Extract the command without the $( and ) wrapper + command = value[2:-1] + try: + result = subprocess_utils.run( + command, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + universal_newlines=True, + ) + output = result.stdout.strip() + logger.info(f"Successfully executed command for Docker {var_name}") + return output + except subprocess.CalledProcessError as e: + logger.error(f"Failed to execute command for Docker {var_name}: {e}") + logger.error(f"Command stderr: {e.stderr}") + raise ValueError( + f"Command execution failed for Docker {var_name}. " + f"Error: {e.stderr.strip() or str(e)}. " + f"If you meant to use the literal string '{value}' as the {var_name}, " + f"please escape it with a backslash: '\\{value}'" + ) from e + + return value # Copied from ray.autoscaler._private.ray_constants @@ -216,7 +276,9 @@ def initialize(self) -> str: # SkyPilot: Docker login if user specified a private docker registry. if 'docker_login_config' in self.docker_config: - # TODO(tian): Maybe support a command to get the login password? + # Docker login credentials can be specified as executable commands in + # the format "$(command)", which will be executed to get the actual value. + # To use a literal $(command) string, escape it with a backslash: \$(command) docker_login_config = DockerLoginConfig( **self.docker_config['docker_login_config']) self._run(