Skip to content
10 changes: 9 additions & 1 deletion src/deadline/client/cli/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,15 @@ def _apply_cli_options_to_config(
)
else:
# Remove the standard option names from the args list
for name in ["profile", "farm_id", "region", "queue_id", "job_id", "storage_profile_id"]:
for name in [
"profile",
"farm_id",
"region",
"queue_id",
"job_id",
"storage_profile_id",
"conflict_resolution",
]:
args.pop(name, None)

# Check that the required options have values, auto-selecting if only one exists
Expand Down
10 changes: 8 additions & 2 deletions src/deadline/client/cli/_groups/attachment_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,10 @@ def attachment_download(
if not s3_settings:
raise MissingJobAttachmentSettingsError(f"Queue {queue_id} has no attachment settings")

s3_root_uri = s3_settings.to_s3_root_uri()
# Only fall back to the queue's S3 settings when the caller did not provide an
# explicit --s3-root-uri. An explicitly-supplied value must always be honored.
if not s3_root_uri:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new comment says an explicit --s3-root-uri must be honored "even when the queue has no attachment settings of its own", but get_queue(...) (lines 126-130) is still called unconditionally before this check. So when a caller supplies --s3-root-uri precisely to operate without relying on queue settings, they still incur a GetQueue call — and if that call fails (e.g. AccessDenied, or the queue does not exist), the command errors out before ever reaching this branch, defeating the intent. Consider moving the get_queue(...) lookup inside the if not s3_root_uri: block so it is only performed when the fallback is actually needed.

s3_root_uri = s3_settings.to_s3_root_uri()
Comment thread
crowecawcaw marked this conversation as resolved.

deadline_client = get_session_client(boto3_session, "deadline", region=region)
boto3_session = api.get_queue_user_boto3_session(deadline=deadline_client, config=config)
Expand Down Expand Up @@ -243,7 +246,10 @@ def attachment_upload(
if not s3_settings:
raise MissingJobAttachmentSettingsError(f"Queue {queue_id} has no attachment settings")

s3_root_uri = s3_settings.to_s3_root_uri()
# Only fall back to the queue's S3 settings when the caller did not provide an
# explicit --s3-root-uri. An explicitly-supplied value must always be honored.
if not s3_root_uri:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as in attachment_download: get_queue(...) (lines 244-248) is still called unconditionally, so an explicit --s3-root-uri does not actually let a caller bypass the queue lookup. If GetQueue fails (AccessDenied, missing queue), upload errors out before reaching this fallback branch, contradicting the "must always be honored, even when the queue has no attachment settings" comment. Move the get_queue(...) call inside if not s3_root_uri:.

s3_root_uri = s3_settings.to_s3_root_uri()
Comment thread
crowecawcaw marked this conversation as resolved.

deadline_client = get_session_client(boto3_session, "deadline", region=region)
boto3_session = api.get_queue_user_boto3_session(deadline=deadline_client, config=config)
Expand Down
168 changes: 168 additions & 0 deletions test/unit/deadline_client/cli/test_cli_attachment_args.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

"""
Tests for argument plumbing in the `deadline attachment` CLI group:
* `--conflict-resolution` must not trip the `_apply_cli_options_to_config`
"not standard CLI options" RuntimeError guard in the config-defaults workflow.
* `--s3-root-uri` must be honored independently of `--profile`.
"""

from __future__ import annotations

import json
from unittest.mock import MagicMock, patch

import pytest
from click.testing import CliRunner

from deadline.client.cli import main
from deadline.client.config import config_file
from deadline.client.cli._groups import attachment_group
from deadline.job_attachments.models import JobAttachmentS3Settings
from deadline.job_attachments.progress_tracker import DownloadSummaryStatistics
from ..shared_constants import MOCK_FARM_ID, MOCK_QUEUE_ID

MOCK_REGION = "eu-central-1"

MOCK_S3_SETTINGS = JobAttachmentS3Settings(s3BucketName="mock-bucket", rootPrefix="MockRootPrefix")


def _write_manifest(tmp_path):
manifest_path = tmp_path / "abc123_manifest"
manifest_path.write_text(
json.dumps({"hashAlg": "xxh128", "manifestVersion": "2023-03-03", "paths": []})
)
return str(manifest_path)


@pytest.fixture
def configured_farm_region(fresh_deadline_config):
config_file.set_setting("defaults.farm_id", MOCK_FARM_ID)
config_file.set_setting("defaults.queue_id", MOCK_QUEUE_ID)
config_file.set_setting("defaults.farm_region", MOCK_REGION)
yield fresh_deadline_config


def test_attachment_download_config_defaults_no_conflict_resolution(
configured_farm_region, tmp_path
):
"""
C12: In the config-defaults workflow (no --profile, no --conflict-resolution on the
CLI), the unset --conflict-resolution option must not survive into the
`_apply_cli_options_to_config` args and trip the "not standard CLI options" RuntimeError.
"""
manifest_path = _write_manifest(tmp_path)

mock_queue = MagicMock()
mock_queue.jobAttachmentSettings = MOCK_S3_SETTINGS

with (
patch.object(attachment_group.api, "get_boto3_session", return_value=MagicMock()),
patch.object(attachment_group, "get_queue", return_value=mock_queue),
patch.object(attachment_group, "get_session_client", return_value=MagicMock()),
patch.object(
attachment_group.api, "get_queue_user_boto3_session", return_value=MagicMock()
),
patch.object(
attachment_group,
"_attachment_download",
return_value=DownloadSummaryStatistics(),
),
):
runner = CliRunner()
result = runner.invoke(
main,
[
"attachment",
"download",
"--manifests",
manifest_path,
],
)

assert result.exit_code == 0, result.output
assert "not standard AWS Deadline Cloud CLI options" not in result.output


def test_attachment_download_honors_s3_root_uri_without_profile(configured_farm_region, tmp_path):
"""
Bug: --s3-root-uri must be honored even when --profile is not passed. It must not be
overwritten by the queue's job-attachment settings.
"""
manifest_path = _write_manifest(tmp_path)

explicit_uri = "s3://my-explicit-bucket/my-explicit-prefix"

mock_queue = MagicMock()
mock_queue.jobAttachmentSettings = MOCK_S3_SETTINGS

with (
patch.object(attachment_group.api, "get_boto3_session", return_value=MagicMock()),
patch.object(attachment_group, "get_queue", return_value=mock_queue),
patch.object(attachment_group, "get_session_client", return_value=MagicMock()),
patch.object(
attachment_group.api, "get_queue_user_boto3_session", return_value=MagicMock()
),
patch.object(
attachment_group,
"_attachment_download",
return_value=DownloadSummaryStatistics(),
) as mock_download,
):
runner = CliRunner()
result = runner.invoke(
main,
[
"attachment",
"download",
"--manifests",
manifest_path,
"--s3-root-uri",
explicit_uri,
],
)

assert result.exit_code == 0, result.output
mock_download.assert_called_once()
assert mock_download.call_args.kwargs["s3_root_uri"] == explicit_uri


def test_attachment_upload_honors_s3_root_uri_without_profile(configured_farm_region, tmp_path):
"""
Bug: --s3-root-uri must be honored on `upload` even when --profile is not passed. It
must not be overwritten by the queue's job-attachment settings.
"""
manifest_path = _write_manifest(tmp_path)

explicit_uri = "s3://my-explicit-bucket/my-explicit-prefix"

mock_queue = MagicMock()
mock_queue.jobAttachmentSettings = MOCK_S3_SETTINGS

with (
patch.object(attachment_group.api, "get_boto3_session", return_value=MagicMock()),
patch.object(attachment_group, "get_queue", return_value=mock_queue),
patch.object(attachment_group, "get_session_client", return_value=MagicMock()),
patch.object(
attachment_group.api, "get_queue_user_boto3_session", return_value=MagicMock()
),
patch.object(
attachment_group, "_attachment_upload", return_value=MagicMock()
) as mock_upload,
):
runner = CliRunner()
result = runner.invoke(
main,
[
"attachment",
"upload",
"--manifests",
manifest_path,
"--s3-root-uri",
explicit_uri,
],
)

assert result.exit_code == 0, result.output
mock_upload.assert_called_once()
assert mock_upload.call_args.kwargs["s3_root_uri"] == explicit_uri
Loading