From 08d4e885221e6b26f8eb3577f057171b260c6302 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 3 Oct 2025 11:32:02 +0300 Subject: [PATCH 1/5] Add AWS console URL builders for ECS resources - Implement build_cluster_url() for cluster console links - Implement build_service_url() for service console links - Implement build_task_url() for task console links - Extract task ID from ARN or use as-is if already an ID - Add comprehensive tests for URL construction - Handle special characters in resource names --- src/lazy_ecs/core/aws_console.py | 26 ++++++++++++++++++ tests/test_aws_console.py | 45 ++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/lazy_ecs/core/aws_console.py create mode 100644 tests/test_aws_console.py diff --git a/src/lazy_ecs/core/aws_console.py b/src/lazy_ecs/core/aws_console.py new file mode 100644 index 0000000..3157a18 --- /dev/null +++ b/src/lazy_ecs/core/aws_console.py @@ -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 diff --git a/tests/test_aws_console.py b/tests/test_aws_console.py new file mode 100644 index 0000000..bc3ca01 --- /dev/null +++ b/tests/test_aws_console.py @@ -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 From 0b58fe77c4bd9c8be2137bf50a9217f4d4674ab9 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 3 Oct 2025 11:35:24 +0300 Subject: [PATCH 2/5] Add 'Open in AWS console' for services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add '🌐 Open in AWS console' menu option to service actions - Implement open_service_in_console() in ECSNavigator - Add get_region() method to ECSService - Open service URL in default browser using webbrowser - Display URL before opening for user awareness --- src/lazy_ecs/__init__.py | 4 ++++ src/lazy_ecs/aws_service.py | 4 ++++ src/lazy_ecs/features/service/ui.py | 1 + src/lazy_ecs/ui.py | 11 +++++++++++ tests/test_service_ui.py | 2 +- 5 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/lazy_ecs/__init__.py b/src/lazy_ecs/__init__.py index 89e1374..9c0b767 100644 --- a/src/lazy_ecs/__init__.py +++ b/src/lazy_ecs/__init__.py @@ -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 diff --git a/src/lazy_ecs/aws_service.py b/src/lazy_ecs/aws_service.py index beb7d6f..66aafa8 100644 --- a/src/lazy_ecs/aws_service.py +++ b/src/lazy_ecs/aws_service.py @@ -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 diff --git a/src/lazy_ecs/features/service/ui.py b/src/lazy_ecs/features/service/ui.py index 2a45e8d..4a110ec 100644 --- a/src/lazy_ecs/features/service/ui.py +++ b/src/lazy_ecs/features/service/ui.py @@ -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( diff --git a/src/lazy_ecs/ui.py b/src/lazy_ecs/ui.py index 3b4e7ff..cfde05a 100644 --- a/src/lazy_ecs/ui.py +++ b/src/lazy_ecs/ui.py @@ -113,6 +113,17 @@ 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 _build_task_feature_choices(containers: list[dict[str, Any]]) -> list[dict[str, str]]: """Build feature menu choices for containers plus navigation options.""" diff --git a/tests/test_service_ui.py b/tests/test_service_ui.py index 93b200b..24bcbc6 100644 --- a/tests/test_service_ui.py +++ b/tests/test_service_ui.py @@ -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") From ce3e5a4a48e9081e6fd65781cf257cba58123784 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 3 Oct 2025 11:44:31 +0300 Subject: [PATCH 3/5] Add 'Open in AWS console' for tasks --- src/lazy_ecs/__init__.py | 2 ++ src/lazy_ecs/features/task/ui.py | 1 + src/lazy_ecs/ui.py | 11 +++++++++++ tests/test_task_ui.py | 2 +- 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/lazy_ecs/__init__.py b/src/lazy_ecs/__init__.py index 9c0b767..86ed4fd 100644 --- a/src/lazy_ecs/__init__.py +++ b/src/lazy_ecs/__init__.py @@ -199,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__": diff --git a/src/lazy_ecs/features/task/ui.py b/src/lazy_ecs/features/task/ui.py index 8a82afa..579d6e4 100644 --- a/src/lazy_ecs/features/task/ui.py +++ b/src/lazy_ecs/features/task/ui.py @@ -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"}, ] ) diff --git a/src/lazy_ecs/ui.py b/src/lazy_ecs/ui.py index cfde05a..d59be28 100644 --- a/src/lazy_ecs/ui.py +++ b/src/lazy_ecs/ui.py @@ -124,6 +124,17 @@ def open_service_in_console(self, cluster_name: str, service_name: str) -> None: 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.""" diff --git a/tests/test_task_ui.py b/tests/test_task_ui.py index cab0291..1ce2fe2 100644 --- a/tests/test_task_ui.py +++ b/tests/test_task_ui.py @@ -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 From 577d437d73a94f786ff3cf6a9a45580acfdb5d10 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 3 Oct 2025 11:44:45 +0300 Subject: [PATCH 4/5] update roadmap --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d3bdff9..77762e0 100644 --- a/README.md +++ b/README.md @@ -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 From 37365a58e42ba00679c13d01a1d2e6d0d6811be6 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 3 Oct 2025 11:46:44 +0300 Subject: [PATCH 5/5] bump version to 0.5.0 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7d7d3e4..e5f0971 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ diff --git a/uv.lock b/uv.lock index 2315c8b..2f6c636 100644 --- a/uv.lock +++ b/uv.lock @@ -384,7 +384,7 @@ wheels = [ [[package]] name = "lazy-ecs" -version = "0.4.0" +version = "0.5.0" source = { editable = "." } dependencies = [ { name = "boto3" },