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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
# --------------------------------------------------------------------------------------------
# pylint: disable=line-too-long, consider-using-f-string, no-else-return, duplicate-string-formatting-argument, expression-not-assigned, too-many-locals, logging-fstring-interpolation, arguments-differ, abstract-method, logging-format-interpolation, broad-except

import os

from azure.cli.core.azclierror import InvalidArgumentValueError
from knack.log import get_logger
from knack.prompting import prompt, prompt_choice_list

Expand Down Expand Up @@ -211,6 +214,47 @@ def resolve_registry_from_cli_args(registry_server, registry_user, registry_pass
return (registry_server, registry_user, registry_pass)


def _resolve_compose_file_path(file_path, project_directory, field):
if not isinstance(file_path, str) or not file_path or os.path.isabs(file_path) or os.path.splitdrive(file_path)[0]:
raise InvalidArgumentValueError(
f"'{field}' must be a relative file path within the Compose file's directory.")

try:
resolved_path = os.path.realpath(os.path.join(project_directory, file_path), strict=True)
if os.path.normcase(os.path.commonpath([project_directory, resolved_path])) != os.path.normcase(project_directory):
raise InvalidArgumentValueError(
f"'{field}' must reference a file within the Compose file's directory.")
except (OSError, ValueError) as ex:
raise InvalidArgumentValueError(f"Unable to resolve the file referenced by '{field}': {ex}") from ex
Comment on lines +227 to +228

if not os.path.isfile(resolved_path):
raise InvalidArgumentValueError(f"'{field}' must reference a regular file.")
return resolved_path


def validate_compose_file_paths(compose_file, compose_file_path):
project_directory = os.path.realpath(os.path.dirname(os.path.abspath(compose_file_path)))
validated_secrets = set()
for service_name, service in compose_file.services.items():
if service.env_file is not None:
service.env_file[:] = [
_resolve_compose_file_path(file_path, project_directory, f"services.{service_name}.env_file")
for file_path in service.env_file
]
for secret in service.secrets or []:
if secret.source in validated_secrets:
continue
if compose_file.secrets is None or secret.source not in compose_file.secrets:
raise InvalidArgumentValueError(
f"Service '{service_name}' references an undefined secret '{secret.source}'.")
secret_config = compose_file.secrets[secret.source]
if secret_config is not None and secret_config.file is not None:
resolved_path = _resolve_compose_file_path(
secret_config.file, project_directory, f"secrets.{secret.source}.file")
secret_config.file = type(secret_config.file)(resolved_path)
validated_secrets.add(secret.source)


def resolve_environment_from_service(service):
env_array = []

Expand Down
5 changes: 5 additions & 0 deletions src/azure-cli/azure/cli/command_modules/containerapp/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -1631,6 +1631,11 @@
helps['containerapp compose create'] = """
type: command
short-summary: Create one or more Container Apps in a new or existing Container App Environment from a Compose specification.
long-summary: |
Secret files and env_file entries must use relative paths within the Compose file's directory.
Paths are resolved relative to that directory, not the current working directory. Absolute paths and
symbolic links that resolve outside that directory are not supported. Files are checked before deployment.
Values loaded from env_file are stored as ordinary environment variables, not Container App secrets.
examples:
- name: Create a container app by implicitly passing in a Compose configuration file from current directory.
text: |
Expand Down
26 changes: 20 additions & 6 deletions src/azure-cli/azure/cli/command_modules/containerapp/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
ResourceNotFoundError,
CLIError,
CLIInternalError,
FileOperationError,
InvalidArgumentValueError,
ArgumentUsageError,
MutuallyExclusiveArgumentError)
Expand Down Expand Up @@ -4917,7 +4918,24 @@ def create_containerapps_from_compose(cmd, # pylint: disable=R0914
resolve_memory_configuration_from_service,
resolve_replicas_from_service,
resolve_environment_from_service,
resolve_secret_from_service)
resolve_secret_from_service,
validate_compose_file_paths)

compose_yaml = load_yaml_file(compose_file_path)
parsed_compose_file = ComposeFile(compose_yaml)
validate_compose_file_paths(parsed_compose_file, compose_file_path)
logger.info(parsed_compose_file)
service_files = {}
for service_name in parsed_compose_file.ordered_services:
service = parsed_compose_file.services[service_name]
try:
environment = resolve_environment_from_service(service)
secret_vars, secret_env_ref = resolve_secret_from_service(service, parsed_compose_file.secrets)
except (OSError, UnicodeError) as ex:
raise FileOperationError(
f"Unable to read Compose files for service '{service_name}'. "
"Check file permissions and text encoding.") from ex
service_files[service_name] = (environment, secret_vars, secret_env_ref)

# Validate managed environment
parsed_managed_env = parse_resource_id(managed_env)
Expand All @@ -4938,9 +4956,6 @@ def create_containerapps_from_compose(cmd, # pylint: disable=R0914
tags=tags,
location=location)

compose_yaml = load_yaml_file(compose_file_path)
parsed_compose_file = ComposeFile(compose_yaml)
logger.info(parsed_compose_file)
containerapps_from_compose = []
# Using the key to iterate to get the service name
# pylint: disable=C0201,C0206
Expand All @@ -4964,8 +4979,7 @@ def create_containerapps_from_compose(cmd, # pylint: disable=R0914
managed_environment
)
replicas = resolve_replicas_from_service(service)
environment = resolve_environment_from_service(service)
secret_vars, secret_env_ref = resolve_secret_from_service(service, parsed_compose_file.secrets)
environment, secret_vars, secret_env_ref = service_files[service_name]
if environment is not None and secret_env_ref is not None:
environment.extend(secret_env_ref)
elif secret_env_ref is not None:
Expand Down
Loading
Loading