Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/c7n/resources/ecs.py: 44%
552 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
1# Copyright The Cloud Custodian Authors.
2# SPDX-License-Identifier: Apache-2.0
3from botocore.exceptions import ClientError
5from c7n.actions import BaseAction
6from c7n.exceptions import PolicyExecutionError
7from c7n.filters import MetricsFilter, ValueFilter, Filter
8from c7n.filters.offhours import OffHour, OnHour
9from c7n.manager import resources
10from c7n.utils import local_session, chunks, get_retry, type_schema, group_by, jmespath_compile
11from c7n import query, utils
12from c7n.query import DescribeSource, ConfigSource
13from c7n.tags import Tag, TagDelayedAction, RemoveTag, TagActionFilter
14from c7n.actions import AutoTagUser, AutoscalingBase
15import c7n.filters.vpc as net_filters
18def ecs_tag_normalize(resources):
19 """normalize tag format on ecs resources to match common aws format."""
20 for r in resources:
21 if 'tags' in r:
22 r['Tags'] = [{'Key': t['key'], 'Value': t['value']} for t in r['tags']]
23 r.pop('tags')
26NEW_ARN_STYLE = ('container-instance', 'service', 'task')
29def ecs_taggable(model, r):
30 # Tag support requires new arn format
31 # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-using-tags.html
32 #
33 # New arn format details
34 # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-resource-ids.html
35 #
36 path_parts = r[model.id].rsplit(':', 1)[-1].split('/')
37 if path_parts[0] not in NEW_ARN_STYLE:
38 return True
39 return len(path_parts) > 2
42class ContainerConfigSource(ConfigSource):
44 preserve_empty = ()
45 preserve_case = {'Tags'}
46 mapped_keys = {}
48 @classmethod
49 def remap_keys(cls, resource):
50 for k, v in cls.mapped_keys.items():
51 if v in resource:
52 continue
53 if k not in resource:
54 continue
55 resource[v] = resource.pop(k)
56 return resource
58 @classmethod
59 def lower_keys(cls, data):
60 if isinstance(data, dict):
61 for k, v in list(data.items()):
62 if k in cls.preserve_case:
63 continue
64 lk = k[0].lower() + k[1:]
65 data[lk] = data.pop(k)
66 # describe doesn't return empty list/dict by default
67 if isinstance(v, (list, dict)) and not v and lk not in cls.preserve_empty:
68 data.pop(lk)
69 elif isinstance(v, (dict, list)):
70 data[lk] = cls.lower_keys(v)
71 elif isinstance(data, list):
72 return list(map(cls.lower_keys, data))
73 return data
75 def load_resource(self, item):
76 resource = self.lower_keys(super().load_resource(item))
77 if self.mapped_keys:
78 return self.remap_keys(resource)
79 return resource
82class ClusterDescribe(query.DescribeSource):
84 def augment(self, resources):
85 resources = super(ClusterDescribe, self).augment(resources)
86 ecs_tag_normalize(resources)
87 return resources
90@resources.register('ecs')
91class ECSCluster(query.QueryResourceManager):
93 class resource_type(query.TypeInfo):
94 service = 'ecs'
95 enum_spec = ('list_clusters', 'clusterArns', None)
96 batch_detail_spec = (
97 'describe_clusters', 'clusters', None, 'clusters', {'include': ['TAGS', 'SETTINGS']})
98 name = "clusterName"
99 arn = id = "clusterArn"
100 arn_type = 'cluster'
101 config_type = cfn_type = 'AWS::ECS::Cluster'
103 source_mapping = {
104 'describe': ClusterDescribe,
105 'config': query.ConfigSource
106 }
109@ECSCluster.filter_registry.register('metrics')
110class ECSMetrics(MetricsFilter):
112 def get_dimensions(self, resource):
113 return [{'Name': 'ClusterName', 'Value': resource['clusterName']}]
116class ECSClusterResourceDescribeSource(query.ChildDescribeSource):
118 # We need an additional subclass of describe for ecs cluster.
119 #
120 # - Default child query just returns the child resources from
121 # enumeration op, for ecs clusters, enumeration just returns
122 # resources ids, we also need to retain the parent id for
123 # augmentation.
124 #
125 # - The default augmentation detail_spec/batch_detail_spec need additional
126 # handling for the string resources with parent id.
127 #
129 def __init__(self, manager):
130 self.manager = manager
131 self.query = query.ChildResourceQuery(
132 self.manager.session_factory, self.manager)
133 self.query.capture_parent_id = True
135 def get_resources(self, ids, cache=True):
136 """Retrieve ecs resources for serverless policies or related resources
138 Requires arns in new format.
139 https://docs.aws.amazon.com/AmazonECS/latest/userguide/ecs-resource-ids.html
140 """
141 cluster_resources = {}
142 for i in ids:
143 _, ident = i.rsplit(':', 1)
144 parts = ident.split('/', 2)
145 if len(parts) != 3:
146 raise PolicyExecutionError("New format ecs arn required")
147 cluster_resources.setdefault(parts[1], []).append(parts[2])
149 results = []
150 client = local_session(self.manager.session_factory).client('ecs')
151 for cid, resource_ids in cluster_resources.items():
152 results.extend(
153 self.process_cluster_resources(client, cid, resource_ids))
154 return results
156 def augment(self, resources):
157 parent_child_map = {}
158 for pid, r in resources:
159 parent_child_map.setdefault(pid, []).append(r)
160 results = []
161 with self.manager.executor_factory(
162 max_workers=self.manager.max_workers) as w:
163 client = local_session(self.manager.session_factory).client('ecs')
164 futures = {}
165 for pid, services in parent_child_map.items():
166 futures[
167 w.submit(
168 self.process_cluster_resources, client, pid, services)
169 ] = (pid, services)
170 for f in futures:
171 pid, services = futures[f]
172 if f.exception():
173 self.manager.log.warning(
174 'error fetching ecs resources for cluster %s: %s',
175 pid, f.exception())
176 continue
177 results.extend(f.result())
178 return results
181@query.sources.register('describe-ecs-service')
182class ECSServiceDescribeSource(ECSClusterResourceDescribeSource):
184 def process_cluster_resources(self, client, cluster_id, services):
185 results = []
186 for service_set in chunks(services, self.manager.chunk_size):
187 results.extend(
188 client.describe_services(
189 cluster=cluster_id,
190 include=['TAGS'],
191 services=service_set).get('services', []))
192 ecs_tag_normalize(results)
193 return results
196class ECSServiceConfigSource(ContainerConfigSource):
197 perserve_empty = {
198 'placementConstraints', 'placementStrategy',
199 'serviceRegistries', 'Tags', 'loadBalancers'}
201 mapped_keys = {
202 'role': 'roleArn', 'cluster': 'clusterArn'}
205@resources.register('ecs-service')
206class Service(query.ChildResourceManager):
208 chunk_size = 10
210 class resource_type(query.TypeInfo):
211 service = 'ecs'
212 name = 'serviceName'
213 arn = id = 'serviceArn'
214 enum_spec = ('list_services', 'serviceArns', None)
215 parent_spec = ('ecs', 'cluster', None)
216 supports_trailevents = True
217 config_type = cfn_type = 'AWS::ECS::Service'
219 source_mapping = {
220 'config': ECSServiceConfigSource,
221 'describe-child': ECSServiceDescribeSource,
222 'describe': ECSServiceDescribeSource,
223 }
225 def get_resources(self, ids, cache=True, augment=True):
226 return super(Service, self).get_resources(ids, cache, augment=False)
229@Service.filter_registry.register('metrics')
230class ServiceMetrics(MetricsFilter):
232 def get_dimensions(self, resource):
233 return [
234 {'Name': 'ClusterName', 'Value': resource['clusterArn'].rsplit('/')[-1]},
235 {'Name': 'ServiceName', 'Value': resource['serviceName']}]
238class RelatedTaskDefinitionFilter(ValueFilter):
240 schema = type_schema('task-definition', rinherit=ValueFilter.schema)
241 schema_alias = False
242 permissions = ('ecs:DescribeTaskDefinition',
243 'ecs:ListTaskDefinitions')
244 related_key = 'taskDefinition'
246 def process(self, resources, event=None):
247 task_def_ids = list({s[self.related_key] for s in resources})
248 task_def_manager = self.manager.get_resource_manager(
249 'ecs-task-definition')
251 # due to model difference (multi-level containment with
252 # multi-step resource iteration) and potential volume of
253 # resources, we break our abstractions a little in the name of
254 # efficiency wrt api usage.
256 # check to see if task def cache is already populated
257 key = task_def_manager.get_cache_key(None)
258 if self.manager._cache.get(key):
259 task_defs = task_def_manager.get_resources(task_def_ids)
260 # else just augment the ids
261 else:
262 task_defs = task_def_manager.augment(task_def_ids)
263 self.task_defs = {t['taskDefinitionArn']: t for t in task_defs}
264 return super(RelatedTaskDefinitionFilter, self).process(resources)
266 def __call__(self, i):
267 task = self.task_defs[i[self.related_key]]
268 return self.match(task)
271@Service.filter_registry.register('task-definition')
272class ServiceTaskDefinitionFilter(RelatedTaskDefinitionFilter):
273 """Filter services by their task definitions.
275 :Example:
277 Find any fargate services that are running with a particular
278 image in the task and stop them.
280 .. code-block:: yaml
282 policies:
283 - name: fargate-find-stop-image
284 resource: ecs-task
285 filters:
286 - launchType: FARGATE
287 - type: task-definition
288 key: "containerDefinitions[].image"
289 value: "elasticsearch/elasticsearch:6.4.3"
290 value_type: swap
291 op: contains
292 actions:
293 - type: stop
294 """
296@ECSCluster.filter_registry.register('ebs-storage')
297class Storage(ValueFilter):
298 """Filter clusters by configured EBS storage parameters.
300 :Example:
302 Find any ECS clusters that have instances that are using unencrypted EBS volumes.
304 .. code-block:: yaml
306 policies:
307 - name: encrypted-ebs-volumes
308 resource: ecs
309 filters:
310 - type: ebs-storage
311 key: Encrypted
312 value: true
313 """
315 schema = type_schema(
316 'ebs-storage', rinherit=ValueFilter.schema,
317 operator={'type': 'string', 'enum': ['or', 'and']},
318 )
319 schema_alias = False
321 def get_permissions(self):
322 return (self.manager.get_resource_manager('ebs').get_permissions() +
323 self.manager.get_resource_manager('ec2').get_permissions() +
324 self.manager.get_resource_manager('ecs').get_permissions()
325 )
327 def process(self, resources, event=None):
328 self.storage = self.get_storage(resources)
329 self.skip = []
330 self.operator = self.data.get(
331 'operator', 'or') == 'or' and any or all
332 return list(filter(self, resources))
334 def get_storage(self, resources):
335 manager = self.manager.get_resource_manager('ecs-container-instance')
337 storage = {}
338 for cluster_set in utils.chunks(resources, 200):
339 for cluster in cluster_set:
340 cluster["clusterArn"]
341 instances = manager.resources({
342 "cluster": cluster['clusterArn'],
343 }, augment=False)
344 instances = manager.get_resources(instances, augment=False)
345 storage[cluster["clusterArn"]] = []
347 for instance in instances:
348 storage[cluster["clusterArn"]].extend(self.get_ebs_volumes([instance["ec2InstanceId"]]))
350 return storage
352 def get_ebs_volumes(self, resources):
353 volumes = []
354 ec2_manager = self.manager.get_resource_manager('ec2')
355 ebs_manager = self.manager.get_resource_manager('ebs')
356 for instance_set in utils.chunks(resources, 200):
357 instance_set = ec2_manager.get_resources(instance_set)
358 volume_ids = []
359 for i in instance_set:
360 for bd in i.get('BlockDeviceMappings', ()):
361 if 'Ebs' not in bd:
362 continue
363 volume_ids.append(bd['Ebs']['VolumeId'])
364 for v in ebs_manager.get_resources(volume_ids):
365 if not v['Attachments']:
366 continue
367 volumes.append(v)
368 return volumes
370 def __call__(self, i):
371 storage = self.storage.get(i["clusterArn"])
373 if not storage:
374 return False
375 return self.operator(map(self.match, storage))
378@Service.filter_registry.register('subnet')
379class SubnetFilter(net_filters.SubnetFilter):
381 RelatedIdsExpression = ""
382 expressions = ('taskSets[].networkConfiguration.awsvpcConfiguration.subnets[]',
383 'deployments[].networkConfiguration.awsvpcConfiguration.subnets[]',
384 'networkConfiguration.awsvpcConfiguration.subnets[]')
386 def get_related_ids(self, resources):
387 subnet_ids = set()
388 for exp in self.expressions:
389 cexp = jmespath_compile(exp)
390 for r in resources:
391 ids = cexp.search(r)
392 if ids:
393 subnet_ids.update(ids)
394 return list(subnet_ids)
397@Service.filter_registry.register('security-group')
398class SGFilter(net_filters.SecurityGroupFilter):
400 RelatedIdsExpression = ""
401 expressions = ('taskSets[].networkConfiguration.awsvpcConfiguration.securityGroups[]',
402 'deployments[].networkConfiguration.awsvpcConfiguration.securityGroups[]',
403 'networkConfiguration.awsvpcConfiguration.securityGroups[]')
405 def get_related_ids(self, resources):
406 sg_ids = set()
407 for exp in self.expressions:
408 cexp = jmespath_compile(exp)
409 for r in resources:
410 ids = cexp.search(r)
411 if ids:
412 sg_ids.update(ids)
413 return list(sg_ids)
416@Service.filter_registry.register('network-location', net_filters.NetworkLocation)
419@Service.action_registry.register('modify')
420class UpdateService(BaseAction):
421 """Action to update service
423 :example:
425 .. code-block:: yaml
427 policies:
428 - name: no-public-ips-services
429 resource: ecs-service
430 filters:
431 - 'networkConfiguration.awsvpcConfiguration.assignPublicIp': 'ENABLED'
432 actions:
433 - type: modify
434 update:
435 networkConfiguration:
436 awsvpcConfiguration:
437 assignPublicIp: DISABLED
438 """
440 schema = type_schema('modify',
441 update={
442 'desiredCount': {'type': 'integer'},
443 'taskDefinition': {'type': 'string'},
444 'deploymentConfiguration': {
445 'type': 'object',
446 'properties': {
447 'maximumPercent': {'type': 'integer'},
448 'minimumHealthyPercent': {'type': 'integer'},
449 }
450 },
451 'networkConfiguration': {
452 'type': 'object',
453 'properties': {
454 'awsvpcConfiguration': {
455 'type': 'object',
456 'properties': {
457 'subnets': {
458 'type': 'array',
459 'items': {
460 'type': 'string',
461 },
462 'minItems': 1
463 },
464 'securityGroups': {
465 'items': {
466 'type': 'string',
467 },
468 },
469 'assignPublicIp': {
470 'type': 'string',
471 'enum': ['ENABLED', 'DISABLED'],
472 }
473 }
474 }
475 }
476 },
477 'platformVersion': {'type': 'string'},
478 'forceNewDeployment': {'type': 'boolean', 'default': False},
479 'healthCheckGracePeriodSeconds': {'type': 'integer'},
480 }
481 )
483 permissions = ('ecs:UpdateService',)
485 def process(self, resources):
486 client = local_session(self.manager.session_factory).client('ecs')
487 update = self.data.get('update')
489 for r in resources:
490 param = {}
492 # Handle network separately as it requires atomic updating, and populating
493 # defaults from the resource.
494 net_update = update.get('networkConfiguration', {}).get('awsvpcConfiguration')
495 if net_update:
496 net_param = dict(r['networkConfiguration']['awsvpcConfiguration'])
497 param['networkConfiguration'] = {'awsvpcConfiguration': net_param}
498 for k, v in net_update.items():
499 net_param[k] = v
501 for k, v in update.items():
502 if k == 'networkConfiguration':
503 continue
504 elif r.get(k) != v:
505 param[k] = v
507 if not param:
508 continue
510 client.update_service(
511 cluster=r['clusterArn'], service=r['serviceName'], **param)
514@Service.action_registry.register('delete')
515class DeleteService(BaseAction):
516 """Delete service(s)."""
518 schema = type_schema('delete')
519 permissions = ('ecs:DeleteService',)
521 def process(self, resources):
522 client = local_session(self.manager.session_factory).client('ecs')
523 retry = get_retry(('Throttling',))
524 for r in resources:
525 try:
526 primary = [d for d in r['deployments']
527 if d['status'] == 'PRIMARY'].pop()
528 if primary['desiredCount'] > 0:
529 retry(client.update_service,
530 cluster=r['clusterArn'],
531 service=r['serviceName'],
532 desiredCount=0)
533 retry(client.delete_service,
534 cluster=r['clusterArn'], service=r['serviceName'])
535 except ClientError as e:
536 if e.response['Error']['Code'] != 'ServiceNotFoundException':
537 raise
540@query.sources.register('describe-ecs-task')
541class ECSTaskDescribeSource(ECSClusterResourceDescribeSource):
543 def process_cluster_resources(self, client, cluster_id, tasks):
544 results = []
545 for task_set in chunks(tasks, self.manager.chunk_size):
546 results.extend(
547 self.manager.retry(
548 client.describe_tasks,
549 cluster=cluster_id,
550 include=['TAGS'],
551 tasks=task_set).get('tasks', []))
552 ecs_tag_normalize(results)
553 return results
556@resources.register('ecs-task')
557class Task(query.ChildResourceManager):
559 chunk_size = 100
561 class resource_type(query.TypeInfo):
562 service = 'ecs'
563 arn = id = name = 'taskArn'
564 arn_type = 'task'
565 enum_spec = ('list_tasks', 'taskArns', None)
566 parent_spec = ('ecs', 'cluster', None)
567 supports_trailevents = True
568 cfn_type = 'AWS::ECS::TaskSet'
570 @property
571 def source_type(self):
572 source = self.data.get('source', 'describe')
573 if source in ('describe', 'describe-child'):
574 source = 'describe-ecs-task'
575 return source
577 def get_resources(self, ids, cache=True, augment=True):
578 return super(Task, self).get_resources(ids, cache, augment=False)
581@Task.filter_registry.register('subnet')
582class TaskSubnetFilter(net_filters.SubnetFilter):
584 RelatedIdsExpression = "attachments[].details[?name == 'subnetId'].value[]"
587@Task.filter_registry.register('security-group')
588class TaskSGFilter(net_filters.SecurityGroupFilter):
590 ecs_group_cache = None
592 RelatedIdsExpression = ""
593 eni_expression = "attachments[].details[?name == 'networkInterfaceId'].value[]"
594 sg_expression = "Groups[].GroupId[]"
596 def _get_related_ids(self, resources):
597 groups = dict()
598 eni_ids = set()
600 cexp = jmespath_compile(self.eni_expression)
601 for r in resources:
602 ids = cexp.search(r)
603 if ids:
604 eni_ids.update(ids)
606 if eni_ids:
607 client = local_session(self.manager.session_factory).client('ec2')
608 response = client.describe_network_interfaces(
609 NetworkInterfaceIds=list(eni_ids)
610 )
611 if response["NetworkInterfaces"]:
612 cexp = jmespath_compile(self.sg_expression)
613 for r in response["NetworkInterfaces"]:
614 ids = cexp.search(r)
615 if ids:
616 groups[r["NetworkInterfaceId"]] = ids
617 self.ecs_group_cache = groups
619 return groups
621 def get_related_ids(self, resources):
622 if not self.ecs_group_cache:
623 self.ecs_group_cache = self._get_related_ids(resources)
625 group_ids = set()
626 cexp = jmespath_compile(self.eni_expression)
627 for r in resources:
628 ids = cexp.search(r)
629 for group_id in ids:
630 group_ids.update(self.ecs_group_cache.get(group_id, ()))
631 return list(group_ids)
634@Task.filter_registry.register('network-location', net_filters.NetworkLocation)
637@Task.filter_registry.register('task-definition')
638class TaskTaskDefinitionFilter(RelatedTaskDefinitionFilter):
639 """Filter tasks by their task definition.
641 :Example:
643 Find any fargate tasks that are running without read only root
644 and stop them.
646 .. code-block:: yaml
648 policies:
649 - name: fargate-readonly-tasks
650 resource: ecs-task
651 filters:
652 - launchType: FARGATE
653 - type: task-definition
654 key: "containerDefinitions[].readonlyRootFilesystem"
655 value: None
656 value_type: swap
657 op: contains
658 actions:
659 - type: stop
661 """
662 related_key = 'taskDefinitionArn'
665@Task.action_registry.register('stop')
666class StopTask(BaseAction):
667 """Stop/Delete a currently running task."""
669 schema = type_schema('stop', reason={"type": "string"})
670 permissions = ('ecs:StopTask',)
672 def process(self, resources):
673 client = local_session(self.manager.session_factory).client('ecs')
674 retry = get_retry(('Throttling',))
675 reason = self.data.get('reason', 'custodian policy')
677 for r in resources:
678 try:
679 retry(client.stop_task,
680 cluster=r['clusterArn'],
681 task=r['taskArn'],
682 reason=reason)
683 except ClientError as e:
684 # No error code for not found.
685 if e.response['Error']['Message'] != "The referenced task was not found.":
686 raise
689class DescribeTaskDefinition(DescribeSource):
691 def get_resources(self, ids, cache=True):
692 if cache:
693 resources = self.manager._get_cached_resources(ids)
694 if resources is not None:
695 return resources
696 try:
697 resources = self.augment(ids)
698 return resources
699 except ClientError as e:
700 self.manager.log.warning("event ids not resolved: %s error:%s" % (ids, e))
701 return []
703 def augment(self, resources):
704 results = []
705 client = local_session(self.manager.session_factory).client('ecs')
706 for task_def_set in resources:
707 response = self.manager.retry(
708 client.describe_task_definition,
709 taskDefinition=task_def_set,
710 include=['TAGS'])
711 r = response['taskDefinition']
712 r['tags'] = response.get('tags', [])
713 results.append(r)
714 ecs_tag_normalize(results)
715 return results
718class ConfigECSTaskDefinition(ContainerConfigSource):
720 preserve_empty = {'mountPoints', 'portMappings', 'volumesFrom'}
723@resources.register('ecs-task-definition')
724class TaskDefinition(query.QueryResourceManager):
726 class resource_type(query.TypeInfo):
727 service = 'ecs'
728 arn = id = name = 'taskDefinitionArn'
729 enum_spec = ('list_task_definitions', 'taskDefinitionArns', None)
730 cfn_type = config_type = 'AWS::ECS::TaskDefinition'
731 arn_type = 'task-definition'
733 source_mapping = {
734 'config': ConfigECSTaskDefinition,
735 'describe': DescribeTaskDefinition
736 }
738 def get_resources(self, ids, cache=True, augment=True):
739 return super(TaskDefinition, self).get_resources(ids, cache, augment=False)
742@TaskDefinition.action_registry.register('delete')
743class DeleteTaskDefinition(BaseAction):
744 """Delete/DeRegister a task definition.
746 The definition will be marked as InActive. Currently running
747 services and task can still reference, new services & tasks
748 can't.
750 force is False by default. When given as True, the task definition will
751 be permanently deleted.
753 .. code-block:: yaml
755 policies:
756 - name: deregister-task-definition
757 resource: ecs-task-definition
758 filters:
759 - family: test-task-def
760 actions:
761 - type: delete
763 - name: delete-task-definition
764 resource: ecs-task-definition
765 filters:
766 - family: test-task-def
767 actions:
768 - type: delete
769 force: True
770 """
772 schema = type_schema('delete', force={'type': 'boolean'})
773 permissions = ('ecs:DeregisterTaskDefinition','ecs:DeleteTaskDefinitions',)
775 def process(self, resources):
776 client = local_session(self.manager.session_factory).client('ecs')
777 retry = get_retry(('Throttling',))
778 force = self.data.get('force', False)
780 for r in resources:
781 if r['status'] == 'INACTIVE':
782 continue
783 try:
784 retry(client.deregister_task_definition,
785 taskDefinition=r['taskDefinitionArn'])
786 except ClientError as e:
787 if e.response['Error'][
788 'Message'] != 'The specified task definition does not exist.':
789 raise
791 if force:
792 task_definitions_arns = [
793 r['taskDefinitionArn']
794 for r in resources
795 ]
796 for chunk in chunks(task_definitions_arns, size=10):
797 retry(client.delete_task_definitions, taskDefinitions=chunk)
800@resources.register('ecs-container-instance')
801class ContainerInstance(query.ChildResourceManager):
803 chunk_size = 100
805 class resource_type(query.TypeInfo):
806 service = 'ecs'
807 id = name = 'containerInstanceArn'
808 enum_spec = ('list_container_instances', 'containerInstanceArns', None)
809 parent_spec = ('ecs', 'cluster', None)
810 arn = "containerInstanceArn"
812 @property
813 def source_type(self):
814 source = self.data.get('source', 'describe')
815 if source in ('describe', 'describe-child'):
816 source = 'describe-ecs-container-instance'
817 return source
820@query.sources.register('describe-ecs-container-instance')
821class ECSContainerInstanceDescribeSource(ECSClusterResourceDescribeSource):
823 def process_cluster_resources(self, client, cluster_id, container_instances):
824 results = []
825 for service_set in chunks(container_instances, self.manager.chunk_size):
826 r = client.describe_container_instances(
827 cluster=cluster_id,
828 include=['TAGS'],
829 containerInstances=container_instances).get('containerInstances', [])
830 # Many Container Instance API calls require the cluster_id, adding as a
831 # custodian specific key in the resource
832 for i in r:
833 i['c7n:cluster'] = cluster_id
834 results.extend(r)
835 ecs_tag_normalize(results)
836 return results
839@ContainerInstance.filter_registry.register('subnet')
840class ContainerInstanceSubnetFilter(net_filters.SubnetFilter):
842 RelatedIdsExpression = "attributes[?name == 'ecs.subnet-id'].value[]"
845@ContainerInstance.action_registry.register('set-state')
846class SetState(BaseAction):
847 """Updates a container instance to either ACTIVE or DRAINING
849 :example:
851 .. code-block:: yaml
853 policies:
854 - name: drain-container-instances
855 resource: ecs-container-instance
856 actions:
857 - type: set-state
858 state: DRAINING
859 """
860 schema = type_schema(
861 'set-state',
862 state={"type": "string", 'enum': ['DRAINING', 'ACTIVE']})
863 permissions = ('ecs:UpdateContainerInstancesState',)
865 def process(self, resources):
866 cluster_map = group_by(resources, 'c7n:cluster')
867 for cluster in cluster_map:
868 c_instances = [i['containerInstanceArn'] for i in cluster_map[cluster]
869 if i['status'] != self.data.get('state')]
870 results = self.process_cluster(cluster, c_instances)
871 return results
873 def process_cluster(self, cluster, c_instances):
874 # Limit on number of container instance that can be updated in a single
875 # update_container_instances_state call is 10
876 chunk_size = 10
877 client = local_session(self.manager.session_factory).client('ecs')
878 for service_set in chunks(c_instances, chunk_size):
879 try:
880 client.update_container_instances_state(
881 cluster=cluster,
882 containerInstances=service_set,
883 status=self.data.get('state'))
884 except ClientError:
885 self.manager.log.warning(
886 'Failed to update Container Instances State: %s, cluster %s' %
887 (service_set, cluster))
888 raise
891@ContainerInstance.action_registry.register('update-agent')
892class UpdateAgent(BaseAction):
893 """Updates the agent on a container instance
894 """
896 schema = type_schema('update-agent')
897 permissions = ('ecs:UpdateContainerAgent',)
899 def process(self, resources):
900 client = local_session(self.manager.session_factory).client('ecs')
901 for r in resources:
902 self.process_instance(
903 client, r.get('c7n:cluster'), r.get('containerInstanceArn'))
905 def process_instance(self, client, cluster, instance):
906 try:
907 client.update_container_agent(
908 cluster=cluster, containerInstance=instance)
909 except (client.exceptions.NoUpdateAvailableException,
910 client.exceptions.UpdateInProgressException):
911 return
914@ECSCluster.action_registry.register('tag')
915@TaskDefinition.action_registry.register('tag')
916@Service.action_registry.register('tag')
917@Task.action_registry.register('tag')
918@ContainerInstance.action_registry.register('tag')
919class TagEcsResource(Tag):
920 """Action to create tag(s) on an ECS resource
921 (ecs, ecs-task-definition, ecs-service, ecs-task, ecs-container-instance)
923 Requires arns in new format for tasks, services, and container-instances.
924 https://docs.aws.amazon.com/AmazonECS/latest/userguide/ecs-resource-ids.html
926 :example:
928 .. code-block:: yaml
930 policies:
931 - name: tag-ecs-service
932 resource: ecs-service
933 filters:
934 - "tag:target-tag": absent
935 - type: taggable
936 state: true
937 actions:
938 - type: tag
939 key: target-tag
940 value: target-value
941 """
942 permissions = ('ecs:TagResource',)
943 batch_size = 1
945 def process_resource_set(self, client, resources, tags):
946 mid = self.manager.resource_type.id
947 tags = [{'key': t['Key'], 'value': t['Value']} for t in tags]
948 old_arns = 0
949 for r in resources:
950 if not ecs_taggable(self.manager.resource_type, r):
951 old_arns += 1
952 continue
953 client.tag_resource(resourceArn=r[mid], tags=tags)
954 if old_arns:
955 self.log.warn("Couldn't tag %d resource(s). Needs new ARN format", old_arns)
958@ECSCluster.action_registry.register('remove-tag')
959@TaskDefinition.action_registry.register('remove-tag')
960@Service.action_registry.register('remove-tag')
961@Task.action_registry.register('remove-tag')
962@ContainerInstance.action_registry.register('remove-tag')
963class RemoveTagEcsResource(RemoveTag):
964 """Remove tag(s) from ECS resources
965 (ecs, ecs-task-definition, ecs-service, ecs-task, ecs-container-instance)
967 :example:
969 .. code-block:: yaml
971 policies:
972 - name: ecs-service-remove-tag
973 resource: ecs-service
974 filters:
975 - type: taggable
976 state: true
977 actions:
978 - type: remove-tag
979 tags: ["BadTag"]
980 """
981 permissions = ('ecs:UntagResource',)
982 batch_size = 1
984 def process_resource_set(self, client, resources, keys):
985 old_arns = 0
986 for r in resources:
987 if not ecs_taggable(self.manager.resource_type, r):
988 old_arns += 1
989 continue
990 client.untag_resource(resourceArn=r[self.id_key], tagKeys=keys)
991 if old_arns != 0:
992 self.log.warn("Couldn't untag %d resource(s). Needs new ARN format", old_arns)
995@ECSCluster.action_registry.register('mark-for-op')
996@TaskDefinition.action_registry.register('mark-for-op')
997@Service.action_registry.register('mark-for-op')
998@Task.action_registry.register('mark-for-op')
999@ContainerInstance.action_registry.register('mark-for-op')
1000class MarkEcsResourceForOp(TagDelayedAction):
1001 """Mark ECS resources for deferred action
1002 (ecs, ecs-task-definition, ecs-service, ecs-task, ecs-container-instance)
1004 Requires arns in new format for tasks, services, and container-instances.
1005 https://docs.aws.amazon.com/AmazonECS/latest/userguide/ecs-resource-ids.html
1007 :example:
1009 .. code-block:: yaml
1011 policies:
1012 - name: ecs-service-invalid-tag-stop
1013 resource: ecs-service
1014 filters:
1015 - "tag:InvalidTag": present
1016 - type: taggable
1017 state: true
1018 actions:
1019 - type: mark-for-op
1020 op: delete
1021 days: 1
1022 """
1025@Service.filter_registry.register('taggable')
1026@Task.filter_registry.register('taggable')
1027@ContainerInstance.filter_registry.register('taggable')
1028class ECSTaggable(Filter):
1029 """
1030 Filter ECS resources on arn-format
1031 https://docs.aws.amazon.com/AmazonECS/latest/userguide/ecs-resource-ids.html
1032 :example:
1034 .. code-block:: yaml
1036 policies:
1037 - name: taggable
1038 resource: ecs-service
1039 filters:
1040 - type: taggable
1041 state: True
1042 """
1044 schema = type_schema('taggable', state={'type': 'boolean'})
1046 def get_permissions(self):
1047 return self.manager.get_permissions()
1049 def process(self, resources, event=None):
1050 if not self.data.get('state'):
1051 return [r for r in resources if not ecs_taggable(self.manager.resource_type, r)]
1052 else:
1053 return [r for r in resources if ecs_taggable(self.manager.resource_type, r)]
1056ECSCluster.filter_registry.register('marked-for-op', TagActionFilter)
1057TaskDefinition.filter_registry.register('marked-for-op', TagActionFilter)
1058Service.filter_registry.register('marked-for-op', TagActionFilter)
1059Task.filter_registry.register('marked-for-op', TagActionFilter)
1060ContainerInstance.filter_registry.register('marked-for-op', TagActionFilter)
1062ECSCluster.action_registry.register('auto-tag-user', AutoTagUser)
1063TaskDefinition.action_registry.register('auto-tag-user', AutoTagUser)
1064Service.action_registry.register('auto-tag-user', AutoTagUser)
1065Task.action_registry.register('auto-tag-user', AutoTagUser)
1066ContainerInstance.action_registry.register('auto-tag-user', AutoTagUser)
1068Service.filter_registry.register('offhour', OffHour)
1069Service.filter_registry.register('onhour', OnHour)
1072@Service.action_registry.register('resize')
1073class AutoscalingECSService(AutoscalingBase):
1074 permissions = (
1075 'ecs:UpdateService',
1076 'ecs:TagResource',
1077 'ecs:UntagResource',
1078 )
1080 service_namespace = 'ecs'
1081 scalable_dimension = 'ecs:service:DesiredCount'
1083 def get_resource_id(self, resource):
1084 return resource['serviceArn'].split(':')[-1]
1086 def get_resource_tag(self, resource, key):
1087 if 'Tags' in resource:
1088 for tag in resource['Tags']:
1089 if tag['Key'] == key:
1090 return tag['Value']
1091 return None
1093 def get_resource_desired(self, resource):
1094 return int(resource['desiredCount'])
1096 def set_resource_desired(self, resource, desired):
1097 client = local_session(self.manager.session_factory).client('ecs')
1098 client.update_service(
1099 cluster=resource['clusterArn'],
1100 service=resource['serviceName'],
1101 desiredCount=desired,
1102 )