Skip to content
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
1 change: 1 addition & 0 deletions sdk/ml/azure-ai-ml/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

### Bugs Fixed

- Quote each argument in the local-endpoint `run_cli_command` helper before running it under `shell=True`, so a path containing spaces or shell metacharacters is passed through literally instead of being re-interpreted by the shell.
- Fixed cross-tenant registry endpoint resolution for deployment template operations by using the registry discovery API instead of ARM calls.
- Fixed deployment template update failing with immutable field errors by ensuring `allowedInstanceType` and `allowedEnvironmentVariableOverrides` are properly round-tripped during serialization.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import json
import os
import shlex
import subprocess
import sys
import time
Expand All @@ -27,9 +28,14 @@ def run_cli_command(
if not custom_environment:
custom_environment = os.environ

# We do this join to construct a command because "shell=True" flag, used below, doesn't work with the vector
# argv form on a mac OS.
command_to_execute = " ".join(cmd_arguments)
# "shell=True" (used below) is needed so the platform "az"/"code" wrappers resolve, and it doesn't work with
# the vector argv form on macOS, so the arguments are collapsed into a single command string. Quote each
# argument while joining so a value containing spaces or shell metacharacters is passed through literally
# instead of being re-interpreted by the shell.
if os.name == "nt":
command_to_execute = subprocess.list2cmdline(cmd_arguments)
else:
command_to_execute = shlex.join(cmd_arguments)
Comment on lines +31 to +38

if not do_not_print: # Avoid printing the az login service principal password, for example
print("Preparing to run CLI command: \n{}\n".format(command_to_execute))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# ---------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# ---------------------------------------------------------


import os
import shlex
import subprocess
from unittest.mock import patch

import pytest

from azure.ai.ml._local_endpoints.utilities.commandline_utility import run_cli_command


def _expected_command(args):
return subprocess.list2cmdline(args) if os.name == "nt" else shlex.join(args)


@pytest.mark.unittest
class TestRunCliCommand:
def test_arguments_are_quoted_before_shell_execution(self):
# An argument carrying shell metacharacters (e.g. a deployment-derived path component).
args = ["echo", "x$(touch pwned)"]

with patch(
"azure.ai.ml._local_endpoints.utilities.commandline_utility.subprocess.check_output",
return_value=b"",
) as mock_check_output:
run_cli_command(args)

command_to_execute = mock_check_output.call_args[0][0]
# The raw space-join would let the shell evaluate the $(...) substitution.
assert command_to_execute != " ".join(args)
assert command_to_execute == _expected_command(args)

def test_plain_uri_argument_is_unchanged(self):
uri = "vscode-remote://dev-container+deadbeef/var/azureml-app/onlinescoring"
args = ["code", "--folder-uri", uri]

with patch(
"azure.ai.ml._local_endpoints.utilities.commandline_utility.subprocess.check_output",
return_value=b"",
) as mock_check_output:
run_cli_command(args)

command_to_execute = mock_check_output.call_args[0][0]
assert command_to_execute == _expected_command(args)
if os.name != "nt":
assert command_to_execute == f"code --folder-uri {uri}"