Skip to content

Commit 4a3c40e

Browse files
Merge pull request #42 from ivyleavedtoadflax/feature/add_ecs_module
Add ECS module
2 parents def789a + 90a9f07 commit 4a3c40e

File tree

4 files changed

+203
-1
lines changed

4 files changed

+203
-1
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "remote-py"
3-
version = "0.2.2"
3+
version = "0.2.3"
44
description = "Soft wrapper on top of awscli"
55
authors = ["Matthew Upson <matt@mantisnlp.com>"]
66
license = "MIT License"

remotepy/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from remotepy.instance import app as app
88
from remotepy.snapshot import app as snapshot_app
99
from remotepy.volume import app as volume_app
10+
from remotepy.ecs import app as ecs_app
1011

1112
# This means that the instance app is the default app, so we don't need to run
1213
# remote instance, we can just run remote
@@ -23,6 +24,7 @@ def version():
2324
app.add_typer(config_app, name="config")
2425
app.add_typer(snapshot_app, name="snapshot")
2526
app.add_typer(volume_app, name="volume")
27+
app.add_typer(ecs_app, name="ecs")
2628

2729
if __name__ == "__main__":
2830
app()

remotepy/ecs.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
from typing import List
2+
3+
import boto3
4+
import typer
5+
6+
ecs_client = boto3.client("ecs")
7+
8+
app = typer.Typer()
9+
10+
11+
def get_all_clusters() -> List[str]:
12+
"""
13+
Get all ECS clusters
14+
15+
Returns:
16+
list: A list of all ECS clusters
17+
"""
18+
clusters = ecs_client.list_clusters()
19+
20+
return clusters["clusterArns"]
21+
22+
23+
def get_all_services(cluster_name: str) -> List[str]:
24+
"""
25+
Get all ECS services
26+
27+
Args:
28+
cluster_name (str): The name of the cluster
29+
30+
Returns:
31+
list: A list of all ECS services
32+
"""
33+
services = ecs_client.list_services(cluster=cluster_name)
34+
35+
return services["serviceArns"]
36+
37+
38+
def scale_service(cluster_name: str, service_name: str, desired_count: int) -> None:
39+
"""
40+
Scale an ECS service
41+
42+
Args:
43+
cluster_name (str): The name of the cluster
44+
service_name (str): The name of the service
45+
desired_count (int): The desired count of tasks
46+
"""
47+
ecs_client.update_service(
48+
cluster=cluster_name, service=service_name, desiredCount=desired_count
49+
)
50+
51+
52+
def prompt_for_cluster_name() -> str:
53+
"""
54+
Prompt the user to select a cluster
55+
56+
Returns:
57+
str: The name of the selected cluster
58+
"""
59+
clusters = get_all_clusters()
60+
61+
if not clusters:
62+
typer.echo("No clusters found.")
63+
raise typer.Exit()
64+
elif len(clusters) == 1:
65+
typer.secho(f"Using cluster: {clusters[0]}", fg=typer.colors.BLUE)
66+
67+
return clusters[0]
68+
else:
69+
typer.echo("Please select a cluster from the following list:")
70+
71+
for i, cluster in enumerate(clusters):
72+
typer.secho(f"{i+1}. {cluster}", fg=typer.colors.BLUE)
73+
cluster_choice = typer.prompt("Enter the number of the cluster")
74+
75+
return clusters[int(cluster_choice) - 1]
76+
77+
78+
def prompt_for_services_name(cluster_name: str) -> List[str]:
79+
"""
80+
Prompt the user to select one or more services
81+
82+
Args:
83+
cluster_name (str): The name of the cluster
84+
85+
Returns:
86+
List[str]: The names of the selected services
87+
"""
88+
services = get_all_services(cluster_name)
89+
90+
if not services:
91+
typer.echo("No services found.")
92+
raise typer.Exit()
93+
elif len(services) == 1:
94+
typer.secho(f"Using service: {services[0]}", fg=typer.colors.BLUE)
95+
96+
return [services[0]]
97+
else:
98+
typer.secho(
99+
"Please select one or more services from the following list:",
100+
fg=typer.colors.YELLOW,
101+
)
102+
103+
for i, service in enumerate(services):
104+
typer.secho(f"{i+1}. {service}", fg=typer.colors.BLUE)
105+
service_choices = typer.prompt("Enter the numbers of the services (comma separated)")
106+
service_choices = [int(choice.strip()) for choice in service_choices.split(",")]
107+
selected_services = [services[choice - 1] for choice in service_choices]
108+
109+
return selected_services
110+
111+
112+
@app.command(name="list-clusters")
113+
def list_clusters() -> None:
114+
"""
115+
List ECS clusters
116+
"""
117+
clusters = get_all_clusters()
118+
119+
for cluster in clusters:
120+
typer.secho(cluster, fg=typer.colors.BLUE)
121+
122+
123+
@app.command(name="list-services")
124+
def list_services(
125+
cluster_name: str = typer.Argument(None, help="Cluster name")
126+
) -> None:
127+
"""
128+
List ECS services
129+
130+
Args:
131+
cluster_name (str): The name of the cluster
132+
"""
133+
134+
if not cluster_name:
135+
cluster_name = prompt_for_cluster_name()
136+
137+
services = get_all_services(cluster_name)
138+
139+
for service in services:
140+
typer.secho(service, fg=typer.colors.BLUE)
141+
142+
143+
@app.command()
144+
def scale(
145+
cluster_name: str = typer.Argument(None, help="Cluster name"),
146+
service_name: str = typer.Argument(None, help="Service name"),
147+
desired_count: int = typer.Option(
148+
None, "-n", "--count", help="Desired count of tasks"
149+
),
150+
) -> None:
151+
"""
152+
Scale ECS services
153+
"""
154+
155+
if not cluster_name:
156+
cluster_name = prompt_for_cluster_name()
157+
158+
if not service_name:
159+
services = prompt_for_services_name(cluster_name)
160+
else:
161+
services = [service_name]
162+
163+
if not desired_count:
164+
desired_count = typer.prompt("Desired count of tasks: ", default=1, type=int)
165+
166+
for service in services:
167+
confirm_message = f"Do you really want to scale {service} to {desired_count}?"
168+
169+
if typer.confirm(confirm_message):
170+
scale_service(cluster_name, service, desired_count)
171+
typer.secho(
172+
f"Scaled {service} to {desired_count} tasks", fg=typer.colors.GREEN
173+
)

tests/test_ecs.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from unittest.mock import patch
2+
3+
import pytest
4+
import typer
5+
from remotepy.ecs import prompt_for_services_name
6+
7+
8+
def test_prompt_for_services_name_single_service_found(capsys):
9+
with patch("remotepy.ecs.get_all_services", return_value=["test-service"]):
10+
result = prompt_for_services_name("test-cluster")
11+
assert result == ["test-service"]
12+
captured = capsys.readouterr()
13+
assert "Using service: test-service" in captured.out
14+
15+
16+
def test_prompt_for_services_name_multiple_services_found(capsys):
17+
with patch(
18+
"remotepy.ecs.get_all_services",
19+
return_value=["test-service-1", "test-service-2"],
20+
):
21+
with patch("typer.prompt", return_value="1, 2"):
22+
result = prompt_for_services_name("test-cluster")
23+
assert result == ["test-service-1", "test-service-2"]
24+
captured = capsys.readouterr()
25+
assert "Please select one or more services" in captured.out
26+
assert "1. test-service-1" in captured.out
27+
assert "2. test-service-2" in captured.out

0 commit comments

Comments
 (0)