1# Copyright The Cloud Custodian Authors.
2# SPDX-License-Identifier: Apache-2.0
3import c7n.filters.vpc as net_filters
4from c7n.actions import Action
5from c7n.filters.vpc import SecurityGroupFilter, SubnetFilter, VpcFilter
6from c7n.manager import resources
7from c7n.resources.aws import shape_schema
8from c7n import tags, query
9from c7n.query import QueryResourceManager, TypeInfo, DescribeSource, \
10 ChildResourceManager, ChildDescribeSource
11from c7n.utils import local_session, type_schema, get_retry
12from botocore.waiter import WaiterModel, create_waiter_with_client
13from .aws import shape_validate
14from .ecs import ContainerConfigSource
15from c7n.filters.kms import KmsRelatedFilter
16
17
18@query.sources.register('describe-eks-nodegroup')
19class NodeGroupDescribeSource(ChildDescribeSource):
20
21 def get_query(self):
22 return super().get_query(capture_parent_id=True)
23
24 def augment(self, resources):
25 results = []
26 client = local_session(self.manager.session_factory).client('eks')
27 for cluster_name, nodegroup_name in resources:
28 nodegroup = client.describe_nodegroup(
29 clusterName=cluster_name,
30 nodegroupName=nodegroup_name)['nodegroup']
31 if 'tags' in nodegroup:
32 nodegroup['Tags'] = [{'Key': k, 'Value': v} for k, v in nodegroup['tags'].items()]
33 results.append(nodegroup)
34 return results
35
36
37@resources.register('eks-nodegroup')
38class NodeGroup(ChildResourceManager):
39
40 class resource_type(TypeInfo):
41
42 service = 'eks'
43 arn = 'nodegroupArn'
44 arn_type = 'nodegroup'
45 id = 'nodegroupArn'
46 name = 'nodegroupName'
47 enum_spec = ('list_nodegroups', 'nodegroups', None)
48 parent_spec = ('eks', 'clusterName', None)
49 permissions_enum = ('eks:DescribeNodegroup',)
50 date = 'createdAt'
51
52 source_mapping = {
53 'describe-child': NodeGroupDescribeSource,
54 'describe': NodeGroupDescribeSource,
55 }
56
57
58@NodeGroup.action_registry.register('delete')
59class DeleteNodeGroup(Action):
60 """Delete node group(s)."""
61
62 schema = type_schema('delete')
63 permissions = ('eks:DeleteNodegroup',)
64
65 def process(self, resources):
66 client = local_session(self.manager.session_factory).client('eks')
67 retry = get_retry(('Throttling',))
68 for r in resources:
69 try:
70 retry(client.delete_nodegroup,
71 clusterName=r['clusterName'],
72 nodegroupName=r['nodegroupName'])
73 except client.exceptions.ResourceNotFoundException:
74 continue
75
76
77class EKSDescribeSource(DescribeSource):
78
79 def augment(self, resources):
80 resources = super().augment(resources)
81 for r in resources:
82 if 'tags' not in r:
83 continue
84 r['Tags'] = [{'Key': k, 'Value': v} for k, v in r['tags'].items()]
85 return resources
86
87
88class EKSConfigSource(ContainerConfigSource):
89 mapped_keys = {'certificateAuthorityData': 'certificateAuthority'}
90
91
92@resources.register('eks')
93class EKS(QueryResourceManager):
94
95 class resource_type(TypeInfo):
96 service = 'eks'
97 enum_spec = ('list_clusters', 'clusters', None)
98 arn = 'arn'
99 arn_type = 'cluster'
100 detail_spec = ('describe_cluster', 'name', None, 'cluster')
101 id = name = 'name'
102 date = 'createdAt'
103 config_type = cfn_type = 'AWS::EKS::Cluster'
104
105 source_mapping = {
106 'config': EKSConfigSource,
107 'describe': EKSDescribeSource
108 }
109
110
111@EKS.filter_registry.register('subnet')
112class EKSSubnetFilter(SubnetFilter):
113
114 RelatedIdsExpression = "resourcesVpcConfig.subnetIds[]"
115
116
117@EKS.filter_registry.register('security-group')
118class EKSSGFilter(SecurityGroupFilter):
119
120 RelatedIdsExpression = "resourcesVpcConfig.securityGroupIds[]"
121
122
123EKS.filter_registry.register('network-location', net_filters.NetworkLocation)
124
125
126@EKS.filter_registry.register('vpc')
127class EKSVpcFilter(VpcFilter):
128
129 RelatedIdsExpression = 'resourcesVpcConfig.vpcId'
130
131
132@EKS.filter_registry.register('kms-key')
133class KmsFilter(KmsRelatedFilter):
134 RelatedIdsExpression = 'encryptionConfig[].provider.keyArn'
135
136
137@EKS.action_registry.register('tag')
138class EKSTag(tags.Tag):
139
140 permissions = ('eks:TagResource',)
141
142 def process_resource_set(self, client, resource_set, tags):
143 for r in resource_set:
144 try:
145 self.manager.retry(
146 client.tag_resource,
147 resourceArn=r['arn'],
148 tags={t['Key']: t['Value'] for t in tags})
149 except client.exceptions.ResourceNotFoundException:
150 continue
151
152
153EKS.filter_registry.register('marked-for-op', tags.TagActionFilter)
154EKS.action_registry.register('mark-for-op', tags.TagDelayedAction)
155
156
157@EKS.action_registry.register('remove-tag')
158class EKSRemoveTag(tags.RemoveTag):
159
160 permissions = ('eks:UntagResource',)
161
162 def process_resource_set(self, client, resource_set, tags):
163 for r in resource_set:
164 try:
165 self.manager.retry(
166 client.untag_resource,
167 resourceArn=r['arn'], tagKeys=tags)
168 except client.exceptions.ResourceNotFoundException:
169 continue
170
171
172@EKS.action_registry.register('update-config')
173class UpdateConfig(Action):
174
175 schema = type_schema('update-config',
176 **shape_schema(
177 'eks', 'UpdateClusterConfigRequest', drop_fields=('name'))
178 )
179
180 permissions = ('eks:UpdateClusterConfig',)
181 shape = 'UpdateClusterConfigRequest'
182
183 def validate(self):
184 cfg = dict(self.data)
185 cfg['name'] = 'validate'
186 cfg.pop('type')
187 return shape_validate(
188 cfg, self.shape, self.manager.resource_type.service)
189
190 def process(self, resources):
191 client = local_session(self.manager.session_factory).client('eks')
192 state_filtered = 0
193 params = dict(self.data)
194 params.pop('type')
195 for r in resources:
196 if r['status'] != 'ACTIVE':
197 state_filtered += 1
198 continue
199 client.update_cluster_config(name=r['name'], **params)
200 if state_filtered:
201 self.log.warning(
202 "Filtered %d of %d clusters due to state", state_filtered, len(resources))
203
204
205@EKS.action_registry.register('associate-encryption-config')
206class AssociateEncryptionConfig(Action):
207 """
208 Action that adds an encryption configuration to an EKS cluster.
209
210 :example:
211
212 This policy will find all EKS clusters that do not have Secrets encryption set and
213 associate encryption config with the specified keyArn.
214
215 .. code-block:: yaml
216
217 policies:
218 - name: associate-encryption-config
219 resource: aws.eks
220 filters:
221 - type: value
222 key: encryptionConfig[].provider.keyArn
223 value: absent
224 actions:
225 - type: associate-encryption-config
226 encryptionConfig:
227 - provider:
228 keyArn: alias/eks
229 resources:
230 - secrets
231 """
232 schema = {
233 'type': 'object',
234 'additionalProperties': False,
235 'properties': {
236 'type': {'enum': ['associate-encryption-config']},
237 'encryptionConfig': {
238 'type': 'array',
239 'properties': {
240 'type': 'object',
241 'properties': {
242 'provider': {
243 'type': 'object',
244 'properties': {
245 'keyArn': {'type': 'string'}
246 }
247 },
248 'resources': {
249 'type': 'array',
250 'properties': {
251 'enum': 'secrets'
252 }
253 }
254 }
255 }
256 }
257 }
258 }
259
260 permissions = ('eks:AssociateEncryptionConfig', 'kms:DescribeKey',)
261
262 def process(self, resources):
263 client = local_session(self.manager.session_factory).client('eks')
264 error = None
265 params = dict(self.data)
266 params.pop('type')
267 # associate_encryption_config does not accept kms key aliases, if provided
268 # with an alias find the key arn with kms:DescribeKey first.
269 key_arn = params['encryptionConfig'][0]['provider']['keyArn']
270 if 'alias' in key_arn:
271 try:
272 kms_client = local_session(self.manager.session_factory).client('kms')
273 _key_arn = kms_client.describe_key(KeyId=key_arn)['KeyMetadata']['Arn']
274 params['encryptionConfig'][0]['provider']['keyArn'] = _key_arn
275 except kms_client.exceptions.NotFoundException as e:
276 self.log.error(
277 "The following error was received for kms:DescribeKey: "
278 f"{e.response['Error']['Message']}"
279 )
280 raise e
281 for r in self.filter_resources(resources, 'status', ('ACTIVE',)):
282 try:
283 client.associate_encryption_config(
284 clusterName=r['name'],
285 encryptionConfig=params['encryptionConfig']
286 )
287 except client.exceptions.InvalidParameterException as e:
288 error = e
289 self.log.error(
290 "The following error was received for cluster "
291 f"{r['name']}: {e.response['Error']['Message']}"
292 )
293 continue
294 if error:
295 raise error
296
297
298@EKS.action_registry.register('delete')
299class Delete(Action):
300
301 schema = type_schema('delete')
302 permissions = ('eks:DeleteCluster',)
303
304 def process(self, resources):
305 client = local_session(self.manager.session_factory).client('eks')
306 for r in resources:
307 try:
308 self.delete_associated(r, client)
309 client.delete_cluster(name=r['name'])
310 except client.exceptions.ResourceNotFoundException:
311 continue
312
313 def delete_associated(self, r, client):
314 nodegroups = client.list_nodegroups(clusterName=r['name'])['nodegroups']
315 fargate_profiles = client.list_fargate_profiles(
316 clusterName=r['name'])['fargateProfileNames']
317 waiters = []
318 if nodegroups:
319 for nodegroup in nodegroups:
320 self.manager.retry(
321 client.delete_nodegroup, clusterName=r['name'], nodegroupName=nodegroup)
322 # Nodegroup supports parallel delete so process in parallel, check these later on
323 waiters.append({"clusterName": r['name'], "nodegroupName": nodegroup})
324 if fargate_profiles:
325 waiter = self.fargate_delete_waiter(client)
326 for profile in fargate_profiles:
327 self.manager.retry(
328 client.delete_fargate_profile,
329 clusterName=r['name'], fargateProfileName=profile)
330 # Fargate profiles don't support parallel deletes so process serially
331 waiter.wait(
332 clusterName=r['name'], fargateProfileName=profile)
333 if waiters:
334 waiter = client.get_waiter('nodegroup_deleted')
335 for w in waiters:
336 waiter.wait(**w)
337
338 def fargate_delete_waiter(self, client):
339 # Fargate profiles seem to delete faster @ roughly 2 minutes each so keeping defaults
340 config = {
341 'version': 2,
342 'waiters': {
343 "FargateProfileDeleted": {
344 'operation': 'DescribeFargateProfile',
345 'delay': 30,
346 'maxAttempts': 40,
347 'acceptors': [
348 {
349 "expected": "DELETE_FAILED",
350 "matcher": "path",
351 "state": "failure",
352 "argument": "fargateprofile.status"
353 },
354 {
355 "expected": "ResourceNotFoundException",
356 "matcher": "error",
357 "state": "success"
358 }
359 ]
360 }
361 }
362 }
363 return create_waiter_with_client("FargateProfileDeleted", WaiterModel(config), client)