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

1# Copyright The Cloud Custodian Authors. 

2# SPDX-License-Identifier: Apache-2.0 

3from botocore.exceptions import ClientError 

4 

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 

16 

17 

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') 

24 

25 

26NEW_ARN_STYLE = ('container-instance', 'service', 'task') 

27 

28 

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 

40 

41 

42class ContainerConfigSource(ConfigSource): 

43 

44 preserve_empty = () 

45 preserve_case = {'Tags'} 

46 mapped_keys = {} 

47 

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 

57 

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 

74 

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 

80 

81 

82class ClusterDescribe(query.DescribeSource): 

83 

84 def augment(self, resources): 

85 resources = super(ClusterDescribe, self).augment(resources) 

86 ecs_tag_normalize(resources) 

87 return resources 

88 

89 

90@resources.register('ecs') 

91class ECSCluster(query.QueryResourceManager): 

92 

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' 

102 

103 source_mapping = { 

104 'describe': ClusterDescribe, 

105 'config': query.ConfigSource 

106 } 

107 

108 

109@ECSCluster.filter_registry.register('metrics') 

110class ECSMetrics(MetricsFilter): 

111 

112 def get_dimensions(self, resource): 

113 return [{'Name': 'ClusterName', 'Value': resource['clusterName']}] 

114 

115 

116class ECSClusterResourceDescribeSource(query.ChildDescribeSource): 

117 

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 # 

128 

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 

134 

135 def get_resources(self, ids, cache=True): 

136 """Retrieve ecs resources for serverless policies or related resources 

137 

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]) 

148 

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 

155 

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 

179 

180 

181@query.sources.register('describe-ecs-service') 

182class ECSServiceDescribeSource(ECSClusterResourceDescribeSource): 

183 

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 

194 

195 

196class ECSServiceConfigSource(ContainerConfigSource): 

197 perserve_empty = { 

198 'placementConstraints', 'placementStrategy', 

199 'serviceRegistries', 'Tags', 'loadBalancers'} 

200 

201 mapped_keys = { 

202 'role': 'roleArn', 'cluster': 'clusterArn'} 

203 

204 

205@resources.register('ecs-service') 

206class Service(query.ChildResourceManager): 

207 

208 chunk_size = 10 

209 

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' 

218 

219 source_mapping = { 

220 'config': ECSServiceConfigSource, 

221 'describe-child': ECSServiceDescribeSource, 

222 'describe': ECSServiceDescribeSource, 

223 } 

224 

225 def get_resources(self, ids, cache=True, augment=True): 

226 return super(Service, self).get_resources(ids, cache, augment=False) 

227 

228 

229@Service.filter_registry.register('metrics') 

230class ServiceMetrics(MetricsFilter): 

231 

232 def get_dimensions(self, resource): 

233 return [ 

234 {'Name': 'ClusterName', 'Value': resource['clusterArn'].rsplit('/')[-1]}, 

235 {'Name': 'ServiceName', 'Value': resource['serviceName']}] 

236 

237 

238class RelatedTaskDefinitionFilter(ValueFilter): 

239 

240 schema = type_schema('task-definition', rinherit=ValueFilter.schema) 

241 schema_alias = False 

242 permissions = ('ecs:DescribeTaskDefinition', 

243 'ecs:ListTaskDefinitions') 

244 related_key = 'taskDefinition' 

245 

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') 

250 

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. 

255 

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) 

265 

266 def __call__(self, i): 

267 task = self.task_defs[i[self.related_key]] 

268 return self.match(task) 

269 

270 

271@Service.filter_registry.register('task-definition') 

272class ServiceTaskDefinitionFilter(RelatedTaskDefinitionFilter): 

273 """Filter services by their task definitions. 

274 

275 :Example: 

276 

277 Find any fargate services that are running with a particular 

278 image in the task and stop them. 

279 

280 .. code-block:: yaml 

281 

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 """ 

295 

296@ECSCluster.filter_registry.register('ebs-storage') 

297class Storage(ValueFilter): 

298 """Filter clusters by configured EBS storage parameters. 

299 

300 :Example: 

301 

302 Find any ECS clusters that have instances that are using unencrypted EBS volumes. 

303 

304 .. code-block:: yaml 

305 

306 policies: 

307 - name: encrypted-ebs-volumes 

308 resource: ecs 

309 filters: 

310 - type: ebs-storage 

311 key: Encrypted 

312 value: true 

313 """ 

314 

315 schema = type_schema( 

316 'ebs-storage', rinherit=ValueFilter.schema, 

317 operator={'type': 'string', 'enum': ['or', 'and']}, 

318 ) 

319 schema_alias = False 

320 

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 ) 

326 

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)) 

333 

334 def get_storage(self, resources): 

335 manager = self.manager.get_resource_manager('ecs-container-instance') 

336 

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"]] = [] 

346 

347 for instance in instances: 

348 storage[cluster["clusterArn"]].extend(self.get_ebs_volumes([instance["ec2InstanceId"]])) 

349 

350 return storage 

351 

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 

369 

370 def __call__(self, i): 

371 storage = self.storage.get(i["clusterArn"]) 

372 

373 if not storage: 

374 return False 

375 return self.operator(map(self.match, storage)) 

376 

377 

378@Service.filter_registry.register('subnet') 

379class SubnetFilter(net_filters.SubnetFilter): 

380 

381 RelatedIdsExpression = "" 

382 expressions = ('taskSets[].networkConfiguration.awsvpcConfiguration.subnets[]', 

383 'deployments[].networkConfiguration.awsvpcConfiguration.subnets[]', 

384 'networkConfiguration.awsvpcConfiguration.subnets[]') 

385 

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) 

395 

396 

397@Service.filter_registry.register('security-group') 

398class SGFilter(net_filters.SecurityGroupFilter): 

399 

400 RelatedIdsExpression = "" 

401 expressions = ('taskSets[].networkConfiguration.awsvpcConfiguration.securityGroups[]', 

402 'deployments[].networkConfiguration.awsvpcConfiguration.securityGroups[]', 

403 'networkConfiguration.awsvpcConfiguration.securityGroups[]') 

404 

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) 

414 

415 

416@Service.filter_registry.register('network-location', net_filters.NetworkLocation) 

417 

418 

419@Service.action_registry.register('modify') 

420class UpdateService(BaseAction): 

421 """Action to update service 

422 

423 :example: 

424 

425 .. code-block:: yaml 

426 

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 """ 

439 

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 ) 

482 

483 permissions = ('ecs:UpdateService',) 

484 

485 def process(self, resources): 

486 client = local_session(self.manager.session_factory).client('ecs') 

487 update = self.data.get('update') 

488 

489 for r in resources: 

490 param = {} 

491 

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 

500 

501 for k, v in update.items(): 

502 if k == 'networkConfiguration': 

503 continue 

504 elif r.get(k) != v: 

505 param[k] = v 

506 

507 if not param: 

508 continue 

509 

510 client.update_service( 

511 cluster=r['clusterArn'], service=r['serviceName'], **param) 

512 

513 

514@Service.action_registry.register('delete') 

515class DeleteService(BaseAction): 

516 """Delete service(s).""" 

517 

518 schema = type_schema('delete') 

519 permissions = ('ecs:DeleteService',) 

520 

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 

538 

539 

540@query.sources.register('describe-ecs-task') 

541class ECSTaskDescribeSource(ECSClusterResourceDescribeSource): 

542 

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 

554 

555 

556@resources.register('ecs-task') 

557class Task(query.ChildResourceManager): 

558 

559 chunk_size = 100 

560 

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' 

569 

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 

576 

577 def get_resources(self, ids, cache=True, augment=True): 

578 return super(Task, self).get_resources(ids, cache, augment=False) 

579 

580 

581@Task.filter_registry.register('subnet') 

582class TaskSubnetFilter(net_filters.SubnetFilter): 

583 

584 RelatedIdsExpression = "attachments[].details[?name == 'subnetId'].value[]" 

585 

586 

587@Task.filter_registry.register('security-group') 

588class TaskSGFilter(net_filters.SecurityGroupFilter): 

589 

590 ecs_group_cache = None 

591 

592 RelatedIdsExpression = "" 

593 eni_expression = "attachments[].details[?name == 'networkInterfaceId'].value[]" 

594 sg_expression = "Groups[].GroupId[]" 

595 

596 def _get_related_ids(self, resources): 

597 groups = dict() 

598 eni_ids = set() 

599 

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) 

605 

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 

618 

619 return groups 

620 

621 def get_related_ids(self, resources): 

622 if not self.ecs_group_cache: 

623 self.ecs_group_cache = self._get_related_ids(resources) 

624 

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) 

632 

633 

634@Task.filter_registry.register('network-location', net_filters.NetworkLocation) 

635 

636 

637@Task.filter_registry.register('task-definition') 

638class TaskTaskDefinitionFilter(RelatedTaskDefinitionFilter): 

639 """Filter tasks by their task definition. 

640 

641 :Example: 

642 

643 Find any fargate tasks that are running without read only root 

644 and stop them. 

645 

646 .. code-block:: yaml 

647 

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 

660 

661 """ 

662 related_key = 'taskDefinitionArn' 

663 

664 

665@Task.action_registry.register('stop') 

666class StopTask(BaseAction): 

667 """Stop/Delete a currently running task.""" 

668 

669 schema = type_schema('stop', reason={"type": "string"}) 

670 permissions = ('ecs:StopTask',) 

671 

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') 

676 

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 

687 

688 

689class DescribeTaskDefinition(DescribeSource): 

690 

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 [] 

702 

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 

716 

717 

718class ConfigECSTaskDefinition(ContainerConfigSource): 

719 

720 preserve_empty = {'mountPoints', 'portMappings', 'volumesFrom'} 

721 

722 

723@resources.register('ecs-task-definition') 

724class TaskDefinition(query.QueryResourceManager): 

725 

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' 

732 

733 source_mapping = { 

734 'config': ConfigECSTaskDefinition, 

735 'describe': DescribeTaskDefinition 

736 } 

737 

738 def get_resources(self, ids, cache=True, augment=True): 

739 return super(TaskDefinition, self).get_resources(ids, cache, augment=False) 

740 

741 

742@TaskDefinition.action_registry.register('delete') 

743class DeleteTaskDefinition(BaseAction): 

744 """Delete/DeRegister a task definition. 

745 

746 The definition will be marked as InActive. Currently running 

747 services and task can still reference, new services & tasks 

748 can't. 

749 

750 force is False by default. When given as True, the task definition will 

751 be permanently deleted. 

752 

753 .. code-block:: yaml 

754 

755 policies: 

756 - name: deregister-task-definition 

757 resource: ecs-task-definition 

758 filters: 

759 - family: test-task-def 

760 actions: 

761 - type: delete 

762 

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 """ 

771 

772 schema = type_schema('delete', force={'type': 'boolean'}) 

773 permissions = ('ecs:DeregisterTaskDefinition','ecs:DeleteTaskDefinitions',) 

774 

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) 

779 

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 

790 

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) 

798 

799 

800@resources.register('ecs-container-instance') 

801class ContainerInstance(query.ChildResourceManager): 

802 

803 chunk_size = 100 

804 

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" 

811 

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 

818 

819 

820@query.sources.register('describe-ecs-container-instance') 

821class ECSContainerInstanceDescribeSource(ECSClusterResourceDescribeSource): 

822 

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 

837 

838 

839@ContainerInstance.filter_registry.register('subnet') 

840class ContainerInstanceSubnetFilter(net_filters.SubnetFilter): 

841 

842 RelatedIdsExpression = "attributes[?name == 'ecs.subnet-id'].value[]" 

843 

844 

845@ContainerInstance.action_registry.register('set-state') 

846class SetState(BaseAction): 

847 """Updates a container instance to either ACTIVE or DRAINING 

848 

849 :example: 

850 

851 .. code-block:: yaml 

852 

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',) 

864 

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 

872 

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 

889 

890 

891@ContainerInstance.action_registry.register('update-agent') 

892class UpdateAgent(BaseAction): 

893 """Updates the agent on a container instance 

894 """ 

895 

896 schema = type_schema('update-agent') 

897 permissions = ('ecs:UpdateContainerAgent',) 

898 

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')) 

904 

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 

912 

913 

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) 

922 

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 

925 

926 :example: 

927 

928 .. code-block:: yaml 

929 

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 

944 

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) 

956 

957 

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) 

966 

967 :example: 

968 

969 .. code-block:: yaml 

970 

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 

983 

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) 

993 

994 

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) 

1003 

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 

1006 

1007 :example: 

1008 

1009 .. code-block:: yaml 

1010 

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 """ 

1023 

1024 

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: 

1033 

1034 .. code-block:: yaml 

1035 

1036 policies: 

1037 - name: taggable 

1038 resource: ecs-service 

1039 filters: 

1040 - type: taggable 

1041 state: True 

1042 """ 

1043 

1044 schema = type_schema('taggable', state={'type': 'boolean'}) 

1045 

1046 def get_permissions(self): 

1047 return self.manager.get_permissions() 

1048 

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)] 

1054 

1055 

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) 

1061 

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) 

1067 

1068Service.filter_registry.register('offhour', OffHour) 

1069Service.filter_registry.register('onhour', OnHour) 

1070 

1071 

1072@Service.action_registry.register('resize') 

1073class AutoscalingECSService(AutoscalingBase): 

1074 permissions = ( 

1075 'ecs:UpdateService', 

1076 'ecs:TagResource', 

1077 'ecs:UntagResource', 

1078 ) 

1079 

1080 service_namespace = 'ecs' 

1081 scalable_dimension = 'ecs:service:DesiredCount' 

1082 

1083 def get_resource_id(self, resource): 

1084 return resource['serviceArn'].split(':')[-1] 

1085 

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 

1092 

1093 def get_resource_desired(self, resource): 

1094 return int(resource['desiredCount']) 

1095 

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 )