Skip to content

Commit 478bf6e

Browse files
authored
feat: suggest available resources on CLI access errors (aws-deadline#985)
When users provide an incorrect resource ID and receive an access denied or not found error, the CLI now suggests available resources they have access to. This helps users quickly identify typos in resource IDs. - Add _suggest_resources_on_client_error() helper in _common.py - Add api.search_workers() for consistent worker searching - Update CLI groups to show suggestions on ClientError - Add design doc and unit tests Signed-off-by: Preston Tamkin <845970+prestomation@users.noreply.github.com>
1 parent 7fb2318 commit 478bf6e

12 files changed

Lines changed: 1136 additions & 30 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Design: CLI Resource ID Error Suggestions
2+
3+
## Overview
4+
5+
When users provide an incorrect resource ID (farm, queue, fleet, job, worker, storage profile) and receive an access denied or not found error, the CLI automatically suggests available resources they have access to. This helps users quickly identify and fix typos in resource IDs.
6+
7+
## Implemented Commands
8+
9+
The following commands show resource suggestions on access/not-found errors:
10+
11+
| Command | Suggestions Shown |
12+
|---------|-------------------|
13+
| `deadline bundle submit` | queues → farms (fallback) |
14+
| `deadline farm list` | farms |
15+
| `deadline farm get` | farms |
16+
| `deadline fleet list` | fleets → farms (fallback) |
17+
| `deadline fleet get` | fleets → farms (fallback) |
18+
| `deadline queue list` | queues → farms (fallback) |
19+
| `deadline queue get` | queues → farms (fallback) |
20+
| `deadline queue paramdefs` | queues → farms (fallback) |
21+
| `deadline job list` | jobs → queues → farms (fallback) |
22+
| `deadline job get` | jobs → queues → farms (fallback) |
23+
| `deadline job cancel` | jobs → queues → farms (fallback) |
24+
| `deadline worker list` | workers → fleets (fallback) |
25+
| `deadline worker get` | workers → fleets (fallback) |
26+
27+
## Key Behaviors
28+
29+
- **CLI-only feature**: Library users (`deadline.client.api`) receive original `ClientError` exceptions
30+
- **Operation detection**: Uses `exc.operation_name` attribute for reliable operation identification
31+
- **Fuzzy matching for workers**: When a worker_id is provided, uses `searchTermFilter` with fuzzy matching
32+
- **Fallback hierarchy**: When List APIs fail, falls back up the resource hierarchy
33+
- **Permission hint**: When all List APIs fail, shows hint about missing IAM List permissions
34+
- **Truncation**: Lists are truncated at 10 items with "... and N more" message
35+
36+
## Resource Hierarchy
37+
38+
When an error occurs, the CLI attempts to list resources, falling back up the hierarchy if needed:
39+
40+
```
41+
Farm
42+
├── Queue
43+
│ ├── Job
44+
│ └── Storage Profile
45+
└── Fleet
46+
└── Worker
47+
```
48+
49+
## Output Examples
50+
51+
**Queue ID error with available queues:**
52+
```
53+
Failed to submit the job bundle to AWS Deadline Cloud:
54+
An error occurred (AccessDeniedException) when calling the GetQueue operation: ...
55+
56+
Available queues in farm farm-0123456789abcdef0123456789abcdef:
57+
queue-abcd1234567890abcdef1234567890ab Production Render Queue
58+
queue-efgh5678901234cdef5678901234cdef Test Queue
59+
```
60+
61+
**Farm ID error (cascading fallback):**
62+
```
63+
Failed to get Jobs from Deadline:
64+
An error occurred (ResourceNotFoundException) when calling the SearchJobs operation: ...
65+
66+
Farm farm-wrongid12345678901234567890 may be incorrect. Available farms:
67+
farm-0123456789abcdef0123456789abcdef Studio Farm
68+
farm-abcdef01234567890abcdef012345678 Development Farm
69+
```
70+
71+
**Truncation for long lists:**
72+
```
73+
Available queues in farm farm-0123456789abcdef0123456789abcdef:
74+
queue-abcd1234567890abcdef1234567890ab Production Render Queue
75+
queue-efgh5678901234cdef5678901234cdef Test Queue
76+
... and 8 more
77+
```
78+
79+
**Permission hint when all List APIs fail:**
80+
```
81+
Could not list available resources to suggest alternatives.
82+
This may indicate your IAM policy is missing List permissions.
83+
```
84+
85+
## Handled Error Codes
86+
87+
- `AccessDeniedException` - User lacks permission (likely wrong ID)
88+
- `ResourceNotFoundException` - Resource doesn't exist
89+
- `ValidationException` - Invalid ID format
90+
91+
## Use Cases
92+
93+
### UC1: Mistyped Queue ID
94+
User provides wrong queue ID. CLI suggests available queues in the specified farm.
95+
96+
### UC2: Mistyped Farm ID
97+
User provides wrong farm ID. CLI suggests available farms.
98+
99+
### UC3: Mistyped Farm ID (cascading)
100+
User provides wrong farm ID when specifying a queue. Both GetQueue and ListQueues fail. CLI suggests available farms.
101+
102+
### UC4: Mistyped Fleet ID
103+
User provides wrong fleet ID. CLI suggests available fleets in the farm.
104+
105+
### UC5: Mistyped Job ID
106+
User provides wrong job ID. CLI suggests recent jobs in the queue.
107+
108+
### UC6: Mistyped Worker ID
109+
User provides wrong worker ID. CLI suggests available workers in the fleet.
110+
111+
### UC7: Mistyped Storage Profile ID
112+
User provides wrong storage profile ID. CLI suggests available storage profiles for the queue.

src/deadline/client/cli/_common.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"_ProgressBarCallbackManager",
1616
"_parse_file_parameter",
1717
"_parse_multi_format_parameters",
18+
"_suggest_resources_on_client_error",
1819
]
1920

2021
import sys
@@ -342,3 +343,7 @@ def callback(self, upload_metadata: ProgressReportMetadata) -> bool:
342343
self._exit_stack.close()
343344

344345
return sigint_handler.continue_operation
346+
347+
348+
# Re-export from _suggest_resources for backward compatibility
349+
from ._suggest_resources import _suggest_resources_on_client_error # noqa: E402,F401

src/deadline/client/cli/_groups/_job_helpers.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from ... import api
1515
from ...config import config_file
1616
from ...exceptions import DeadlineOperationError
17-
from .._common import _cli_object_repr
17+
from .._common import _cli_object_repr, _suggest_resources_on_client_error
1818

1919

2020
def _format_timestamp(dt: datetime) -> str:
@@ -144,6 +144,14 @@ def _print_job_details(config: Optional[ConfigParser], job_id: str) -> None:
144144
queue_id = config_file.get_setting("defaults.queue_id", config=config)
145145

146146
deadline = api.get_boto3_client("deadline", config=config)
147-
response = deadline.get_job(farmId=farm_id, queueId=queue_id, jobId=job_id)
147+
try:
148+
response = deadline.get_job(farmId=farm_id, queueId=queue_id, jobId=job_id)
149+
except ClientError as exc:
150+
suggestion = _suggest_resources_on_client_error(
151+
exc, farm_id=farm_id, queue_id=queue_id, config=config
152+
)
153+
raise DeadlineOperationError(
154+
f"Failed to get Job from Deadline:\n{exc}{suggestion}"
155+
) from exc
148156
response.pop("ResponseMetadata", None)
149157
click.echo(_cli_object_repr(response))

src/deadline/client/cli/_groups/bundle_group.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
_handle_error,
3636
_ProgressBarCallbackManager,
3737
_parse_multi_format_parameters,
38+
_suggest_resources_on_client_error,
3839
)
3940
from .._main import deadline as main
4041
from ._sigint_handler import SigIntHandler
@@ -335,8 +336,14 @@ def _check_create_job_wait_canceled() -> bool:
335336
click.echo("Canceled waiting for final status of CreateJob.")
336337
sys.exit(1)
337338
except ClientError as exc:
339+
suggestion = _suggest_resources_on_client_error(
340+
exc,
341+
farm_id=config_file.get_setting("defaults.farm_id", config=config),
342+
queue_id=config_file.get_setting("defaults.queue_id", config=config),
343+
config=config,
344+
)
338345
raise DeadlineOperationError(
339-
f"Failed to submit the job bundle to AWS Deadline Cloud:\n{exc}"
346+
f"Failed to submit the job bundle to AWS Deadline Cloud:\n{exc}{suggestion}"
340347
) from exc
341348
except MisconfiguredInputsError as exc:
342349
click.echo(str(exc))

src/deadline/client/cli/_groups/farm_group.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010
from ... import api
1111
from ...config import config_file
1212
from ...exceptions import DeadlineOperationError
13-
from .._common import _apply_cli_options_to_config, _cli_object_repr, _handle_error
13+
from .._common import (
14+
_apply_cli_options_to_config,
15+
_cli_object_repr,
16+
_handle_error,
17+
_suggest_resources_on_client_error,
18+
)
1419
from .._main import deadline as main
1520

1621

@@ -42,7 +47,10 @@ def farm_list(**args):
4247
try:
4348
response = api.list_farms(config=config)
4449
except ClientError as exc:
45-
raise DeadlineOperationError(f"Failed to get Farms from Deadline:\n{exc}") from exc
50+
suggestion = _suggest_resources_on_client_error(exc, config=config)
51+
raise DeadlineOperationError(
52+
f"Failed to get Farms from Deadline:\n{exc}{suggestion}"
53+
) from exc
4654

4755
# Select which fields to print and in which order
4856
structured_farm_list = [
@@ -68,7 +76,13 @@ def farm_get(**args):
6876
farm_id = config_file.get_setting("defaults.farm_id", config=config)
6977

7078
deadline = api.get_boto3_client("deadline", config=config)
71-
response = deadline.get_farm(farmId=farm_id)
79+
try:
80+
response = deadline.get_farm(farmId=farm_id)
81+
except ClientError as exc:
82+
suggestion = _suggest_resources_on_client_error(exc, farm_id=farm_id, config=config)
83+
raise DeadlineOperationError(
84+
f"Failed to get Farm from Deadline:\n{exc}{suggestion}"
85+
) from exc
7286
response.pop("ResponseMetadata", None)
7387

7488
click.echo(_cli_object_repr(response))

src/deadline/client/cli/_groups/fleet_group.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010
from ... import api
1111
from ...config import config_file
1212
from ...exceptions import DeadlineOperationError
13-
from .._common import _apply_cli_options_to_config, _cli_object_repr, _handle_error
13+
from .._common import (
14+
_apply_cli_options_to_config,
15+
_cli_object_repr,
16+
_handle_error,
17+
_suggest_resources_on_client_error,
18+
)
1419
from .._main import deadline as main
1520

1621

@@ -45,7 +50,10 @@ def fleet_list(**args):
4550
try:
4651
response = api.list_fleets(farmId=farm_id, config=config)
4752
except ClientError as exc:
48-
raise DeadlineOperationError(f"Failed to get Fleets from Deadline:\n{exc}") from exc
53+
suggestion = _suggest_resources_on_client_error(exc, farm_id=farm_id, config=config)
54+
raise DeadlineOperationError(
55+
f"Failed to get Fleets from Deadline:\n{exc}{suggestion}"
56+
) from exc
4957

5058
# Select which fields to print and in which order
5159
structured_fleet_list = [
@@ -90,12 +98,28 @@ def fleet_get(fleet_id, queue_id, **args):
9098
deadline = api.get_boto3_client("deadline", config=config)
9199

92100
if fleet_id:
93-
response = deadline.get_fleet(farmId=farm_id, fleetId=fleet_id)
101+
try:
102+
response = deadline.get_fleet(farmId=farm_id, fleetId=fleet_id)
103+
except ClientError as exc:
104+
suggestion = _suggest_resources_on_client_error(
105+
exc, farm_id=farm_id, fleet_id=fleet_id, config=config
106+
)
107+
raise DeadlineOperationError(
108+
f"Failed to get Fleet from Deadline:\n{exc}{suggestion}"
109+
) from exc
94110
response.pop("ResponseMetadata", None)
95111

96112
click.echo(_cli_object_repr(response))
97113
else:
98-
response = deadline.get_queue(farmId=farm_id, queueId=queue_id)
114+
try:
115+
response = deadline.get_queue(farmId=farm_id, queueId=queue_id)
116+
except ClientError as exc:
117+
suggestion = _suggest_resources_on_client_error(
118+
exc, farm_id=farm_id, queue_id=queue_id, config=config
119+
)
120+
raise DeadlineOperationError(
121+
f"Failed to get Queue from Deadline:\n{exc}{suggestion}"
122+
) from exc
99123
queue_name = response["displayName"]
100124

101125
response = api._list_apis._call_paginated_deadline_list_api(

src/deadline/client/cli/_groups/job_group.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@
4343
from ... import api
4444
from ...config import config_file
4545
from ...exceptions import DeadlineOperationError, DeadlineOperationTimedOut
46-
from .._common import _apply_cli_options_to_config, _cli_object_repr, _handle_error
46+
from .._common import (
47+
_apply_cli_options_to_config,
48+
_cli_object_repr,
49+
_handle_error,
50+
_suggest_resources_on_client_error,
51+
)
4752
from .._main import deadline as main
4853
from ._sigint_handler import SigIntHandler
4954
from ...api._session import get_default_client_config
@@ -140,7 +145,12 @@ def job_list(page_size, item_offset, **args):
140145
sortExpressions=[{"fieldSort": {"name": "CREATED_AT", "sortOrder": "DESCENDING"}}],
141146
)
142147
except ClientError as exc:
143-
raise DeadlineOperationError(f"Failed to get Jobs from Deadline:\n{exc}") from exc
148+
suggestion = _suggest_resources_on_client_error(
149+
exc, farm_id=farm_id, queue_id=queue_id, config=config
150+
)
151+
raise DeadlineOperationError(
152+
f"Failed to get Jobs from Deadline:\n{exc}{suggestion}"
153+
) from exc
144154

145155
total_results = response["totalResults"]
146156

@@ -248,7 +258,15 @@ def job_cancel(mark_as: str, yes: bool, **args):
248258
deadline = api.get_boto3_client("deadline", config=config)
249259

250260
# Print a summary of the job to cancel
251-
job = deadline.get_job(farmId=farm_id, queueId=queue_id, jobId=job_id)
261+
try:
262+
job = deadline.get_job(farmId=farm_id, queueId=queue_id, jobId=job_id)
263+
except ClientError as exc:
264+
suggestion = _suggest_resources_on_client_error(
265+
exc, farm_id=farm_id, queue_id=queue_id, config=config
266+
)
267+
raise DeadlineOperationError(
268+
f"Failed to get Job from Deadline:\n{exc}{suggestion}"
269+
) from exc
252270
# Remove the zero-count status counts
253271
job["taskRunStatusCounts"] = {
254272
name: count for name, count in job["taskRunStatusCounts"].items() if count != 0

src/deadline/client/cli/_groups/queue_group.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@
1919
from ...api._session import get_session_client
2020
from ...config import config_file
2121
from ...exceptions import DeadlineOperationError
22-
from .._common import _apply_cli_options_to_config, _cli_object_repr, _handle_error
22+
from .._common import (
23+
_apply_cli_options_to_config,
24+
_cli_object_repr,
25+
_handle_error,
26+
_suggest_resources_on_client_error,
27+
)
2328
from ....job_attachments.models import (
2429
FileConflictResolution,
2530
)
@@ -65,7 +70,10 @@ def queue_list(**args):
6570
try:
6671
response = api.list_queues(farmId=farm_id, config=config)
6772
except ClientError as exc:
68-
raise DeadlineOperationError(f"Failed to get Queues from Deadline:\n{exc}") from exc
73+
suggestion = _suggest_resources_on_client_error(exc, farm_id=farm_id, config=config)
74+
raise DeadlineOperationError(
75+
f"Failed to get Queues from Deadline:\n{exc}{suggestion}"
76+
) from exc
6977

7078
# Select which fields to print and in which order
7179
structured_queue_list = [
@@ -201,8 +209,11 @@ def queue_paramdefs(**args):
201209
try:
202210
response = api.get_queue_parameter_definitions(farmId=farm_id, queueId=queue_id)
203211
except ClientError as exc:
212+
suggestion = _suggest_resources_on_client_error(
213+
exc, farm_id=farm_id, queue_id=queue_id, config=config
214+
)
204215
raise DeadlineOperationError(
205-
f"Failed to get Queue Parameter Definitions from Deadline:\n{exc}"
216+
f"Failed to get Queue Parameter Definitions from Deadline:\n{exc}{suggestion}"
206217
) from exc
207218

208219
click.echo(_cli_object_repr(response))
@@ -226,7 +237,15 @@ def queue_get(**args):
226237
queue_id = config_file.get_setting("defaults.queue_id", config=config)
227238

228239
deadline = api.get_boto3_client("deadline", config=config)
229-
response = deadline.get_queue(farmId=farm_id, queueId=queue_id)
240+
try:
241+
response = deadline.get_queue(farmId=farm_id, queueId=queue_id)
242+
except ClientError as exc:
243+
suggestion = _suggest_resources_on_client_error(
244+
exc, farm_id=farm_id, queue_id=queue_id, config=config
245+
)
246+
raise DeadlineOperationError(
247+
f"Failed to get Queue from Deadline:\n{exc}{suggestion}"
248+
) from exc
230249
response.pop("ResponseMetadata", None)
231250

232251
click.echo(_cli_object_repr(response))

0 commit comments

Comments
 (0)