Skip to content

Commit a516a09

Browse files
authored
Added support for service level x-aws keys from ecs-plugin (#273)
* Added support for x-aws-role and x-aws-policies * Added support for x-aws-autoscaling * added info in docs
1 parent 1dc4113 commit a516a09

12 files changed

Lines changed: 248 additions & 55 deletions

File tree

docs/extras.rst

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ Extras
77
ECS ComposeX aims to make life easy to take your application to AWS ECS, with using AWS Fargate as the primary
88
focus (still, allows to run on EC2 should you need to).
99

10+
11+
Docker ECS-Plugin x-aws-keys support
12+
=====================================
13+
14+
In order to keep make the integration and inter-operability of tools used by developers, we are going to add support
15+
for, mostly, services level x-aws keys such as **-xaws-iam-role** or **x-aws-autoscaling**.
16+
17+
This will allow developers who might have started a journey to ECS using the docker ecs plugin to continue that journey
18+
with ECS ComposeX without making too many changes.
19+
20+
In case for a similar setting, such as *x-aws-iam-policies* which in ECS Composex is under *x-iam/Policies*, these
21+
non conflicting settings will add up together. However, in case of conflicting information, the ECS ComposeX definition
22+
will prevail over the x-aws-keys.
23+
1024
AWS AppMesh integration
1125
=======================
1226

@@ -52,9 +66,8 @@ numbers.
5266
image: my-nginx
5367
deploy:
5468
replicas: 2 # by default I want 2 containers
55-
x-configs:
56-
scaling:
57-
range: "1-10" # 1 to 10 containers to deploy for the service
69+
x-scaling:
70+
range: "1-10" # 1 to 10 containers to deploy for the service
5871
target_scaling:
5972
cpu_target: 80 # Means 80% average for all containers in the service.
6073
backend:

ecs_composex/common/compose_services.py

Lines changed: 88 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -285,17 +285,26 @@ class ComposeService(object):
285285
("x-network", dict),
286286
]
287287

288+
ecs_plugin_aws_keys = [
289+
("x-aws-role", dict),
290+
("x-aws-policies", list),
291+
("x-aws-autoscaling", dict),
292+
("x-aws-pull_credentials", str),
293+
]
294+
288295
def __init__(self, name, definition, volumes=None, secrets=None):
289296
if not isinstance(definition, dict):
290297
raise TypeError(
291298
"The definition of a service must be", dict, "got", type(definition)
292299
)
293300
if not all(
294-
key in [title[0] for title in self.keys] for key in list(definition.keys())
301+
key in [title[0] for title in self.ecs_plugin_aws_keys + self.keys]
302+
for key in list(definition.keys())
295303
):
296304
raise KeyError(
297305
"Only valid keys for a service definition are",
298306
sorted([key[0] for key in self.keys]),
307+
sorted([key[0] for key in self.ecs_plugin_aws_keys]),
299308
"Got",
300309
sorted(list(definition.keys())),
301310
)
@@ -322,6 +331,8 @@ def __init__(self, name, definition, volumes=None, secrets=None):
322331
self.x_iam = set_else_none("x-iam", self.definition)
323332
self.x_logging = {"RetentionInDays": 14, "CreateLogGroup": True}
324333

334+
self.import_x_aws_settings()
335+
325336
self.replicas = 1
326337
self.container = None
327338
self.volumes = []
@@ -399,6 +410,57 @@ def set_container_definition(self):
399410
)
400411
self.container_parameters.update({self.image_param.title: self.image})
401412

413+
def merge_x_aws_role(self, key):
414+
"""
415+
Method to update the service definition with the x-aws-role information if NOT defined in the composex
416+
definition.
417+
418+
:param str key:
419+
"""
420+
policy_def = {
421+
"PolicyName": "ImportedFromXAWSRole",
422+
"PolicyDocument": self.definition[key],
423+
}
424+
if not self.x_iam:
425+
self.x_iam = {"Policies": [policy_def]}
426+
LOG.info(f"Added {key} definition")
427+
elif self.x_iam and keyisset("Policies", self.x_iam):
428+
self.x_iam["Policies"].append(policy_def)
429+
LOG.info(f"Merged {key} to existing definition")
430+
431+
def merge_x_policies(self, key):
432+
"""
433+
Method to merge policies
434+
435+
:param str key:
436+
"""
437+
if not self.x_iam:
438+
self.x_iam = {"ManagedPolicyArns": self.definition[key]}
439+
LOG.info(f"Added {key} definition")
440+
elif self.x_iam and keyisset("ManagedPolicyArns", self.x_iam):
441+
self.x_iam["ManagedPolicyArns"] += self.definition[key]
442+
LOG.info(f"Merged {key} definition")
443+
444+
def import_x_aws_settings(self):
445+
aws_keys = [
446+
("x-aws-role", dict, self.merge_x_aws_role),
447+
("x-aws-policies", list, self.merge_x_policies),
448+
("x-aws-autoscaling", dict, None),
449+
("x-aws-pull_credentials", str, None),
450+
]
451+
for setting in aws_keys:
452+
if keyisset(setting[0], self.definition) and not isinstance(
453+
self.definition[setting[0]], setting[1]
454+
):
455+
raise TypeError(
456+
f"{setting[0]} is of type",
457+
type(self.definition[setting[0]]),
458+
"Expected",
459+
setting[1],
460+
)
461+
elif keyisset(setting[0], self.definition) and setting[2]:
462+
setting[2](setting[0])
463+
402464
def define_logging(self):
403465
"""
404466
Method to define logging properties
@@ -686,18 +748,22 @@ def add_policies(config, key, new_policies):
686748
if f"PolicyGenerated{count}" not in existing_policy_names
687749
else f"PolicyGenerated{count+len(existing_policy_names)}"
688750
)
689-
name = generated_name if not keyisset("name", policy) else policy["name"]
751+
name = (
752+
generated_name
753+
if not keyisset("PolicyName", policy)
754+
else policy["PolicyName"]
755+
)
690756
if name in existing_policy_names:
691757
return
692-
if not keyisset("document", policy):
758+
if not keyisset("PolicyDocument", policy):
693759
raise KeyError("You must set the policy document for the policy")
694760
if (
695-
keyisset("Version", policy["document"])
696-
and not isinstance(policy["document"]["Version"], str)
697-
or not keyisset("Version", policy["document"])
761+
keyisset("Version", policy["PolicyDocument"])
762+
and not isinstance(policy["PolicyDocument"]["Version"], str)
763+
or not keyisset("Version", policy["PolicyDocument"])
698764
):
699-
policy["document"]["Version"] = "2012-10-17"
700-
policy_object = Policy(PolicyName=name, PolicyDocument=policy["document"])
765+
policy["PolicyDocument"]["Version"] = "2012-10-17"
766+
policy_object = Policy(PolicyName=name, PolicyDocument=policy["PolicyDocument"])
701767
existing_policies.append(policy_object)
702768

703769

@@ -723,7 +789,11 @@ def __init__(self, services, family_name):
723789
self.ignored_services = []
724790
self.name = family_name
725791
self.logical_name = re.sub(r"[^a-zA-Z0-9]+", "", family_name)
726-
self.iam = {"boundary": None, "managed_policies": [], "policies": []}
792+
self.iam = {
793+
"PermissionsBoundary": None,
794+
"ManagedPolicyArns": [],
795+
"Policies": [],
796+
}
727797
self.template = None
728798
self.use_xray = None
729799
self.stack = None
@@ -766,7 +836,7 @@ def set_xray(self):
766836
"resources": {"limits": {"cpus": 0.03125, "memory": "256M"}},
767837
},
768838
"x-iam": {
769-
"managed_policies": [
839+
"ManagedPolicyArns": [
770840
"arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
771841
]
772842
},
@@ -874,9 +944,9 @@ def sort_iam_settings(self, key, setting):
874944

875945
def handle_iam(self):
876946
valid_keys = [
877-
("managed_policies", list, None),
878-
("policies", list, add_policies),
879-
("boundary", (str, Sub), handle_iam_boundary),
947+
("ManagedPolicyArns", list, None),
948+
("Policies", list, add_policies),
949+
("PermissionsBoundary", (str, Sub), handle_iam_boundary),
880950
]
881951
iam_settings = [service.x_iam for service in self.services if service.x_iam]
882952
for setting in iam_settings:
@@ -885,7 +955,7 @@ def handle_iam(self):
885955
self.set_secrets_access()
886956

887957
def handle_permission_boundary(self, prop_key):
888-
if keyisset("boundary", self.iam) and self.template:
958+
if keyisset("PermissionsBoundary", self.iam) and self.template:
889959
if EXEC_ROLE_T in self.template.resources:
890960
add_role_boundaries(
891961
self.template.resources[EXEC_ROLE_T], self.iam[prop_key]
@@ -943,17 +1013,17 @@ def assign_policies(self, role_name=None):
9431013
role = self.template.resources[role_name]
9441014
props = [
9451015
(
946-
"managed_policies",
1016+
"ManagedPolicyArns",
9471017
"ManagedPolicyArns",
9481018
list,
9491019
self.assign_iam_managed_policies,
9501020
),
951-
("policies", "Policies", list, self.assign_iam_policies),
952-
("boundary", "PermissionsBoundary", (str, Sub), None),
1021+
("Policies", "Policies", list, self.assign_iam_policies),
1022+
("PermissionsBoundary", "PermissionsBoundary", (str, Sub), None),
9531023
]
9541024
for prop in props:
9551025
if keyisset(prop[0], self.iam) and isinstance(self.iam[prop[0]], prop[2]):
956-
if prop[0] == "boundary":
1026+
if prop[0] == "PermissionsBoundary":
9571027
self.handle_permission_boundary(prop[0])
9581028
elif prop[3]:
9591029
prop[3](role, prop)

ecs_composex/ecs/ecs_scaling.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,36 @@ def handle_target_scaling(config, key, new_config):
241241
define_new_config(config, key, new_config)
242242

243243

244+
def handle_defined_x_aws_autoscaling(configs, service):
245+
"""
246+
Function to sort out existing or not x-aws-autoscaling in the deploy section
247+
248+
:param list configs:
249+
:param ecs_composex.common.compose_services.ComposeService service:
250+
:return:
251+
"""
252+
if keyisset("deploy", service.definition) and keyisset(
253+
"x-aws-autoscaling", service.definition["deploy"]
254+
):
255+
config = service.definition["deploy"]["x-aws-autoscaling"]
256+
min_count = 1 if not keypresent("min", config) else int(config["min"])
257+
max_count = 1 if not keypresent("max", config) else int(config["max"])
258+
if not service.x_scaling:
259+
service.x_scaling = {"range": f"{min_count}-{max_count}"}
260+
if keyisset("cpu", config):
261+
service.x_scaling.update(
262+
{"target_scaling": {"cpu_target": int(config["cpu"])}}
263+
)
264+
elif service.x_scaling:
265+
LOG.warning(
266+
f"Detected both x-aws-autoscaling and x-scaling for {service.name}. Priority goes to x-scaling"
267+
)
268+
configs.append(service.x_scaling)
269+
elif not keyisset("deploy", service.definition) and service.x_scaling:
270+
LOG.debug("No x-aws-autoscaling detected, proceeding as usual")
271+
configs.append(service.x_scaling)
272+
273+
244274
def merge_family_services_scaling(services):
245275
x_scaling = {
246276
"range": None,
@@ -252,8 +282,10 @@ def merge_family_services_scaling(services):
252282
}
253283
x_scaling_configs = []
254284
for service in services:
255-
if service.x_scaling:
256-
x_scaling_configs.append(service.x_scaling)
285+
handle_defined_x_aws_autoscaling(x_scaling_configs, service)
286+
287+
print(x_scaling_configs)
288+
257289
valid_keys = [
258290
("range", str, handle_range),
259291
("target_scaling", dict, handle_target_scaling),
@@ -266,7 +298,6 @@ def merge_family_services_scaling(services):
266298
and key[2]
267299
):
268300
key[2](x_scaling, key[0], config[key[0]])
269-
270301
return x_scaling
271302

272303

ecs_composex/ecs/ecs_service_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def __init__(self, family, settings):
4646
:param ecs_composex.common.settings.ComposeXSettings settings:
4747
"""
4848
self.network = ServiceNetworking(family)
49-
self.scaling = ServiceScaling(family.services)
49+
self.scaling = ServiceScaling(family.ordered_services)
5050
self.use_appmesh = (
5151
False if not keyisset("x-appmesh", settings.compose_content) else True
5252
)

features/features/common.feature

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Feature: common
4242
Then I render the docker-compose to composex to validate
4343
And I render all files to verify execution
4444
Examples:
45-
| file_path | override_file |
45+
| file_path | override_file |
4646
| use-cases/blog.yml | use-cases/all-in-one.yml |
4747

4848
@cluster
@@ -62,3 +62,11 @@ Feature: common
6262
Examples:
6363
| file_path | override_file |
6464
| use-cases/blog.features.yml | use-cases/logging/variations.yml |
65+
66+
@ecs-plugin-suport
67+
Scenario Outline: ECS Plugin support
68+
Given I use <file_path> as my docker-compose file and <override_file> as override file
69+
Then I render the docker-compose to composex to validate
70+
Examples:
71+
| file_path | override_file |
72+
| use-cases/blog.features.yml | use-cases/ecs_plugin_support/blog.features.x.yml |

use-cases/acm/working_acm.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ services:
5050
x-configs:
5151
use_xray: True
5252
iam:
53-
boundary: arn:aws:iam::aws:policy/PowerUser
53+
PermissionsBoundary: arn:aws:iam::aws:policy/PowerUser
5454

5555
backend:
5656
image: nginx

use-cases/all-in-one.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,18 @@ services:
3232
target: 5000
3333
secrets:
3434
- zyx
35+
x-xray: false
3536
x-iam:
36-
policies:
37-
- document:
37+
Policies:
38+
- PolicyName: toto
39+
PolicyDocument:
3840
Statement:
3941
- Action:
4042
- s3:DeleteBucket
4143
Effect: Deny
4244
Resource:
4345
- '*'
4446
Sid: SomethingStupid
45-
name: toto
46-
x-xray: false
4747
app02:
4848
deploy:
4949
labels:
@@ -72,8 +72,8 @@ services:
7272
- zyx
7373
x-configs:
7474
x-iam:
75-
boundary: ccoe/js-developer
76-
managed_policies:
75+
PermissionsBoundary: ccoe/js-developer
76+
ManagedPolicyArns:
7777
- arn:aws:iam:aws::policy/AdministratorAccess
7878
x-scaling:
7979
range: 1-5
@@ -130,7 +130,7 @@ services:
130130
published: 80
131131
target: 80
132132
x-iam:
133-
managed_policies:
133+
ManagedPolicyArns:
134134
- arn:aws:iam:aws::policy/ReadOnlyAccess
135135
x-scaling:
136136
range: 0-2

use-cases/appmesh/existing_mesh.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,4 @@ x-vpc:
129129
x-configs:
130130
composex:
131131
iam:
132-
boundary: arn:aws:iam::aws:policy/PowerUser
132+
PermissionsBoundary: arn:aws:iam::aws:policy/PowerUser

0 commit comments

Comments
 (0)