Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ lazy-ecs will automatically use the standard AWS credentials chain:

### Quality of Life Features

- **Open resource in AWS console** - One-key shortcut to open current cluster/service/task in browser
- **Open resource in AWS console** - One-key shortcut to open current cluster/service/task in browser

## Development

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "lazy-ecs"
version = "0.4.0"
version = "0.5.0"
description = "A CLI tool for working with AWS services"
readme = "README.md"
authors = [
Expand Down
6 changes: 6 additions & 0 deletions src/lazy_ecs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ def _navigate_services(navigator: ECSNavigator, ecs_service: ECSService, cluster
navigator.show_service_metrics(cluster_name, selected_service)
# Continue the loop to show the menu again

elif selection_type == "action" and action_name == "open_console":
navigator.open_service_in_console(cluster_name, selected_service)
# Continue the loop to show the menu again


def _handle_task_features(
navigator: ECSNavigator, cluster_name: str, task_arn: str, task_details: TaskDetails | None, service_name: str
Expand Down Expand Up @@ -195,6 +199,8 @@ def _handle_task_features(
navigator.show_task_history(cluster_name, service_name)
elif action_name == "show_details":
navigator.display_task_details(task_details)
elif action_name == "open_console":
navigator.open_task_in_console(cluster_name, task_arn)


if __name__ == "__main__":
Expand Down
4 changes: 4 additions & 0 deletions src/lazy_ecs/aws_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,7 @@ def get_service_metrics(self, cluster_name: str, service_name: str, hours: int =
from .features.service.metrics import get_service_metrics

return get_service_metrics(self.cloudwatch_client, cluster_name, service_name, hours)

def get_region(self) -> str:
"""Get the AWS region from the ECS client."""
return self.ecs_client.meta.region_name
26 changes: 26 additions & 0 deletions src/lazy_ecs/core/aws_console.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""AWS Console URL construction for ECS resources."""

from __future__ import annotations


def build_cluster_url(region: str, cluster_name: str) -> str:
"""Build AWS console URL for an ECS cluster."""
return f"https://{region}.console.aws.amazon.com/ecs/v2/clusters/{cluster_name}"


def build_service_url(region: str, cluster_name: str, service_name: str) -> str:
"""Build AWS console URL for an ECS service."""
return f"https://{region}.console.aws.amazon.com/ecs/v2/clusters/{cluster_name}/services/{service_name}"


def build_task_url(region: str, cluster_name: str, task_arn: str) -> str:
"""Build AWS console URL for an ECS task."""
task_id = _extract_task_id(task_arn)
return f"https://{region}.console.aws.amazon.com/ecs/v2/clusters/{cluster_name}/tasks/{task_id}"


def _extract_task_id(task_arn: str) -> str:
"""Extract task ID from task ARN or return as-is if already an ID."""
if task_arn.startswith("arn:aws:ecs:"):
return task_arn.split("/")[-1]
return task_arn
1 change: 1 addition & 0 deletions src/lazy_ecs/features/service/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def select_service_action(self, service_name: str, task_info: list[TaskInfo]) ->

choices.append({"name": "📋 Show service events", "value": "action:show_events"})
choices.append({"name": "📊 Show metrics", "value": "action:show_metrics"})
choices.append({"name": "🌐 Open in AWS console", "value": "action:open_console"})
choices.append({"name": "🚀 Force new deployment", "value": "action:force_deployment"})

return select_with_auto_pagination(
Expand Down
1 change: 1 addition & 0 deletions src/lazy_ecs/features/task/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def select_task_feature(self, task_details: TaskDetails | None) -> str | None:
[
{"name": "Show task details", "value": "task_action:show_details"},
{"name": "Show task history and failures", "value": "task_action:show_history"},
{"name": "🌐 Open in AWS console", "value": "task_action:open_console"},
]
)

Expand Down
22 changes: 22 additions & 0 deletions src/lazy_ecs/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,28 @@ def show_service_metrics(self, cluster_name: str, service_name: str) -> None:
def show_task_history(self, cluster_name: str, service_name: str) -> None:
self._task_ui.display_task_history(cluster_name, service_name)

def open_service_in_console(self, cluster_name: str, service_name: str) -> None:
"""Open the service in AWS console."""
import webbrowser

from .core.aws_console import build_service_url

region = self.ecs_service.get_region()
url = build_service_url(region, cluster_name, service_name)
console.print(f"\n🌐 Opening service in AWS console: {url}", style="cyan")
webbrowser.open(url)

def open_task_in_console(self, cluster_name: str, task_arn: str) -> None:
"""Open the task in AWS console."""
import webbrowser

from .core.aws_console import build_task_url

region = self.ecs_service.get_region()
url = build_task_url(region, cluster_name, task_arn)
console.print(f"\n🌐 Opening task in AWS console: {url}", style="cyan")
webbrowser.open(url)


def _build_task_feature_choices(containers: list[dict[str, Any]]) -> list[dict[str, str]]:
"""Build feature menu choices for containers plus navigation options."""
Expand Down
45 changes: 45 additions & 0 deletions tests/test_aws_console.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Tests for AWS console URL construction."""

from lazy_ecs.core.aws_console import build_cluster_url, build_service_url, build_task_url


def test_build_cluster_url():
url = build_cluster_url(region="us-east-1", cluster_name="production")
assert url == "https://us-east-1.console.aws.amazon.com/ecs/v2/clusters/production"


def test_build_cluster_url_with_special_characters():
url = build_cluster_url(region="eu-west-1", cluster_name="test-cluster-123")
assert url == "https://eu-west-1.console.aws.amazon.com/ecs/v2/clusters/test-cluster-123"


def test_build_service_url():
url = build_service_url(region="us-east-1", cluster_name="production", service_name="web-api")
expected = "https://us-east-1.console.aws.amazon.com/ecs/v2/clusters/production/services/web-api"
assert url == expected


def test_build_service_url_with_special_characters():
url = build_service_url(region="ap-southeast-2", cluster_name="test-cluster", service_name="worker-service-v2")
expected = "https://ap-southeast-2.console.aws.amazon.com/ecs/v2/clusters/test-cluster/services/worker-service-v2"
assert url == expected


def test_build_task_url():
task_arn = "arn:aws:ecs:us-east-1:123456789012:task/production/abc123def456"
url = build_task_url(region="us-east-1", cluster_name="production", task_arn=task_arn)
expected = "https://us-east-1.console.aws.amazon.com/ecs/v2/clusters/production/tasks/abc123def456"
assert url == expected


def test_build_task_url_extracts_task_id_from_arn():
task_arn = "arn:aws:ecs:eu-west-1:987654321098:task/my-cluster/xyz789abc123"
url = build_task_url(region="eu-west-1", cluster_name="my-cluster", task_arn=task_arn)
expected = "https://eu-west-1.console.aws.amazon.com/ecs/v2/clusters/my-cluster/tasks/xyz789abc123"
assert url == expected


def test_build_task_url_with_just_task_id():
url = build_task_url(region="us-west-2", cluster_name="staging", task_arn="simple-task-id-123")
expected = "https://us-west-2.console.aws.amazon.com/ecs/v2/clusters/staging/tasks/simple-task-id-123"
assert url == expected
2 changes: 1 addition & 1 deletion tests/test_service_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def test_select_service_action_with_many_tasks(mock_select, service_ui):

call_args = mock_select.call_args
choices = call_args[0][1]
assert len(choices) == 103 # 100 tasks + 3 actions (events, metrics, deployment)
assert len(choices) == 104 # 100 tasks + 4 actions (events, metrics, console, deployment)


@patch("lazy_ecs.features.service.ui.select_with_auto_pagination")
Expand Down
2 changes: 1 addition & 1 deletion tests/test_task_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,4 @@ def test_select_task_feature_with_many_containers(mock_select, task_ui):

call_args = mock_select.call_args
choices = call_args[0][1]
assert len(choices) == 52
assert len(choices) == 53 # 3 task actions (details, history, console) + 10 containers * 5 actions each
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading