Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support cli var substitution in docker login command env #4871

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/source/examples/docker-containers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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: <your-user-id>.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.
70 changes: 66 additions & 4 deletions sky/provision/docker_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Loading