Skip to content

Commit 9b2a1cd

Browse files
vponomaryovfruch
authored andcommitted
feature(aws): add sct command for cleaning up old AWS KMS aliases
1 parent 335a301 commit 9b2a1cd

File tree

3 files changed

+68
-2
lines changed

3 files changed

+68
-2
lines changed

jenkins-pipelines/qa/hydra-cleanup-cloud.jenkinsfile

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ pipeline {
5252
timeout(time: 15, unit: 'MINUTES') {
5353
sctScript """
5454
./docker/env/hydra.sh python -m utils.cloud_cleanup.aws.clean_aws ${params.dryRun ? '--dry-run' : ''}
55+
./docker/env/hydra.sh python -m sct clean-aws-kms-aliases ${params.dryRun ? '--dry-run' : ''}
5556
"""
5657
}
5758
}

sct.py

+19
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
from sdcm.utils.ci_tools import get_job_name, get_job_url
5757
from sdcm.utils.git import get_git_commit_id, get_git_status_info
5858
from sdcm.utils.argus import get_argus_client
59+
from sdcm.utils.aws_kms import AwsKms
5960
from sdcm.utils.azure_region import AzureRegion
6061
from sdcm.utils.cloud_monitor import cloud_report, cloud_qa_report
6162
from sdcm.utils.cloud_monitor.cloud_monitor import cloud_non_qa_report
@@ -236,6 +237,24 @@ def provision_resources(backend, test_name: str, config: str):
236237
raise ValueError(f"backend {backend} is not supported")
237238

238239

240+
@cli.command("clean-aws-kms-aliases", help="clean AWS KMS old aliases")
241+
@click.option("-r", "--regions", type=CloudRegion(cloud_provider="aws"), multiple=True,
242+
help="List of regions to cover")
243+
@click.option("--time-delta-h", type=int, required=False,
244+
help="Time delta in hours. Used to detect 'old' aliases.")
245+
@click.option("--dry-run", is_flag=True, default=False,
246+
help="Only show result of search not deleting aliases")
247+
@click.pass_context
248+
def clean_aws_kms_aliases(ctx, regions, time_delta_h, dry_run):
249+
"""Clean AWS KMS old aliases."""
250+
add_file_logger()
251+
regions = regions or SCTConfiguration.aws_supported_regions
252+
aws_kms, kwargs = AwsKms(region_names=regions), {"dry_run": dry_run}
253+
if time_delta_h:
254+
kwargs["time_delta_h"] = time_delta_h
255+
aws_kms.cleanup_old_aliases(**kwargs)
256+
257+
239258
@cli.command('clean-resources', help='clean tagged instances in both clouds (AWS/GCE)')
240259
@click.option('--post-behavior', is_flag=True, default=False, help="clean all resources according to post behavior")
241260
@click.option('--user', type=str, help='user name to filter instances by')

sdcm/utils/aws_kms.py

+48-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#
1212
# Copyright (c) 2023 ScyllaDB
1313

14+
import datetime
1415
import logging
1516

1617
import botocore
@@ -29,7 +30,7 @@ class AwsKms:
2930
def __init__(self, region_names):
3031
if not region_names:
3132
raise ValueError("'region_names' parameter cannot be empty")
32-
self.region_names = region_names if isinstance(region_names, list) else [region_names]
33+
self.region_names = region_names if isinstance(region_names, (list, tuple)) else [region_names]
3334
self.mapping = {
3435
region_name: {
3536
'client': boto3.client('kms', region_name=region_name),
@@ -70,7 +71,7 @@ def get_kms_keys(self, region_name, next_marker=None):
7071
if kms_keys.get("NextMarker"):
7172
yield from self.get_kms_keys(region_name=region_name, next_marker=kms_keys["NextMarker"])
7273

73-
def find_or_create_suitable_kms_keys(self):
74+
def find_or_create_suitable_kms_keys(self, only_find=False):
7475
for region_name in self.region_names:
7576
if self.NUM_OF_KMS_KEYS <= len(self.mapping[region_name]['kms_key_ids']):
7677
continue
@@ -87,6 +88,8 @@ def find_or_create_suitable_kms_keys(self):
8788
self.mapping[region_name]['kms_key_ids'].append(current_kms_key_id)
8889
if self.NUM_OF_KMS_KEYS == len(self.mapping[region_name]['kms_key_ids']):
8990
break
91+
if only_find:
92+
continue
9093
while self.NUM_OF_KMS_KEYS > len(self.mapping[region_name]['kms_key_ids']):
9194
self.create_kms_key(region_name)
9295

@@ -152,3 +155,46 @@ def delete_alias(self, kms_key_alias_name, tolerate_errors=True):
152155
LOGGER.debug(exc.response)
153156
if not tolerate_errors:
154157
raise
158+
159+
def cleanup_old_aliases(self, time_delta_h: int = 48, tolerate_errors: bool = True, dry_run=False):
160+
# NOTE: since the KMS alias creation date depends on the time zone of each specific region
161+
# which may easily differ from the timezone of the caller we assume that deviation may be up to 24h.
162+
# So, if it is needed to make sure that some test must have an alias for 24h then
163+
# it is guaranteed only having margin to be '24h' -> 24 + 24 = 48h.
164+
LOGGER.info("KMS: Search for aliases older than '%d' hours", time_delta_h)
165+
alias_allowed_date = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(hours=25)
166+
dry_run_prefix = "[dry-run]" if dry_run else ""
167+
for region_name in self.region_names:
168+
try:
169+
if not self.mapping[region_name].get("kms_key_ids"):
170+
self.find_or_create_suitable_kms_keys(only_find=True)
171+
kms_keys = self.mapping[region_name].get("kms_key_ids", [])
172+
current_client = self.mapping[region_name]['client']
173+
for kms_key_id in kms_keys:
174+
LOGGER.info("KMS: %s[region '%s'][key '%s'] read aliases", dry_run_prefix, region_name, kms_key_id)
175+
current_aliases = current_client.list_aliases(KeyId=kms_key_id, Limit=999)["Aliases"]
176+
# {'AliasName': 'alias/qa-kms-key-for-rotation-1',
177+
# 'CreationDate': datetime.datetime(2023, 8, 11, 18, 33, 12, 45000, tzinfo=tzlocal()), ... }
178+
for current_alias in current_aliases:
179+
current_alias_name = current_alias.get("AliasName", "notfound")
180+
current_alias_creation_date = current_alias.get("CreationDate")
181+
if not current_alias_name.startswith("alias/testid-"):
182+
LOGGER.info(
183+
"KMS: %s[region '%s'][key '%s'] ignore the '%s' alias as not matching",
184+
dry_run_prefix, region_name, kms_key_id, current_alias_name)
185+
continue
186+
if current_alias_creation_date < alias_allowed_date:
187+
LOGGER.info(
188+
"KMS: %s[region '%s'][key '%s'] %s old alias -> '%s' (%s)",
189+
dry_run_prefix, region_name, kms_key_id,
190+
("found" if dry_run else "deleting"),
191+
current_alias_name, current_alias_creation_date)
192+
if not dry_run:
193+
self.delete_alias(current_alias_name, tolerate_errors=tolerate_errors)
194+
except botocore.exceptions.ClientError as exc:
195+
LOGGER.info(
196+
"KMS: failed to process old aliases in the '%s' region: %s",
197+
region_name, exc.response)
198+
if not tolerate_errors:
199+
raise
200+
LOGGER.info("KMS: finished cleaning up old aliases")

0 commit comments

Comments
 (0)