import argparse import dataclasses import json import logging from copy import deepcopy from typing import Tuple import dictdiffer from playhouse.shortcuts import model_to_dict from analyst.libs import metadata, common_cloud_resource_analyzer, util from analyst.libs.consts import * from analyst.libs.util import get_infra_cr_namespace_prefix, timing from analyst.resource_leak import settings from analyst.resource_leak.model import * logger = logging.getLogger('analyst.resource_leak') # TODO(chenfei): # 1. how to mark cloud resource as deleted # 2. some resources don't have field 'network_name' # 3. some networks should be hibernated but not @dataclasses.dataclass class LeakResult: leaked: bool = False reason: str = '' class BaseLeakAnalyst(object): # is used for extracting network name from k8s name, for example, # "prod-1311265642040528896-eks-ap-northeast-2-c78c6da4" NETWORK_NAME_IN_K8S_NAME_RE = re.compile('\w+-\d+-((eks|aws)-\w+-\w+-\d)-') PROJECT_ID_IN_NETWORK_NAMESPACE = re.compile(r'(nightly-ms|staging|prod)-(\d+)') ID_RE = re.compile('^\d+$') SKIP_ANALYZE_FOR_3LAYERS_ARCH = True USEDBY_ALLOW_LIST = {UsedBy.CustomerTiDB, UsedBy.DBaaSControlPlaneShoot} def __init__(self, md: metadata.Metadata): self.md = md def extra_analyze(self, resource: CloudAsset, data: dict): self.fill_ids(resource) self.fill_network_name_if_empty(resource) self.fill_network_namespace_if_empty(resource) self.fill_k8s_name4gardener(resource) def filter(self, resource: CloudAsset) -> Tuple[bool, str]: # this resource is newly created, ignore if (datetime.datetime.now() - resource.resource_created_at).days < 5: return False, "created less than 5 days" if resource.usedby not in self.USEDBY_ALLOW_LIST: return False, "usedby not allow" # skip for deleted cloud resources if resource.resource_deleted_at: return False, "resource deleted" return True, "" def missing_required_fields(self, r: CloudAsset) -> [str]: usedby2required_fields = { UsedBy.CustomerTiDB: [ { 'name': 'cluster_id', 'extra_empty_values': {'admin', 'shared', 'unknown'}, }, ], UsedBy.DBaaSControlPlaneShoot: [ { 'name': 'project_id', 'extra_empty_values': {'admin', 'shared', 'unknown'}, }, { 'name': 'network_name', 'extra_empty_values': None, }, ] } required_fields = usedby2required_fields.get(r.usedby, []) missing_required_fields = [] for each in required_fields: field_name = each['name'] extra_empty_values = each['extra_empty_values'] if util.is_empty(getattr(r, field_name), extra_empty_values=extra_empty_values): missing_required_fields.append(field_name) return missing_required_fields def leaked(self, resource: CloudAsset) -> LeakResult: usedby = resource.usedby if usedby == UsedBy.CustomerTiDB: cluster_alive, reason = self.md.cluster_alive(resource.cluster_id) if not cluster_alive: return LeakResult(True, f'the owner cluster is not alive: {reason}') approute_alive, reason = self.md.approute_alive(resource.cluster_id) if not approute_alive: return LeakResult(True, f'the owner approute is not alive: {reason}') return LeakResult(False, reason) elif usedby == UsedBy.DBaaSControlPlaneShoot: alive, reason = self.md.network_alive(resource.project_id, resource.network_name, resource.provider, resource.product) if not alive: return LeakResult(True, f'the owner network is not alive: {reason}') else: return LeakResult(False, reason) else: # TODO(chenfei): return LeakResult(False, f'unknown usedby: {usedby}') def fill_network_name_if_empty(self, resource: CloudAsset): # get network_name from 'rname' if it looks like # 'shoot--prod--ef939680-natgw-z0' if resource.shoot_id: shoot = self.md.get_shoot(resource.env, resource.shoot_id) if shoot: if not resource.network_name: resource.network_name = shoot.network_name else: logger.warning(f"shoot({resource.env}/{resource.shoot_id}) is not found") # if resource have shoot id but haven't network_name, just use {provider}-{region} as network_name if not resource.network_name: resource.network_name = f'{resource.provider}-{resource.region}' # get network_name from 'rname' if it looks like # "staging-1369847559691368078-eks-us-west-2-09db0064" if not resource.network_name: result = self.NETWORK_NAME_IN_K8S_NAME_RE.match(resource.k8s_name) if result: resource.network_name = result.groups()[0] def fill_network_namespace_if_empty(self, resource: CloudAsset): if resource.shoot_id: shoot = self.md.get_shoot(resource.env, resource.shoot_id) if shoot: if not resource.network_namespace: resource.network_namespace = shoot.network_namespace if not resource.network_namespace and self.ID_RE.match(resource.project_id): resource.network_namespace = f'{get_infra_cr_namespace_prefix(resource.env)}-{resource.project_id}' def fill_ids(self, r: CloudAsset): if r.shoot_id: shoot = self.md.get_shoot(r.env, r.shoot_id) if shoot: if not r.project_id: r.project_id = shoot.project r.tenant_id = shoot.tenant # parse project_id from network_namespace if not r.project_id: result = self.PROJECT_ID_IN_NETWORK_NAMESPACE.match(r.network_namespace or '') if result: r.project_id = result.groups()[1] if not r.tenant_id: project = self.md.id2project.get(r.project_id) if project: r.tenant_id = project.get('tenant_id') def fill_k8s_name4gardener(self, r: CloudAsset): env_str = r.env if r.env == Env.Dev: env_str = 'nightly' if util.is_empty(r.k8s_name) and r.shoot_id: r.k8s_name = f'shoot--{env_str}--{r.shoot_id}' class AWSEC2(BaseLeakAnalyst): USEDBY_ALLOW_LIST = {UsedBy.CustomerTiDB, UsedBy.DBaaSControlPlaneShoot} def extra_analyze(self, resource: CloudAsset, data: dict): super().extra_analyze(resource, data) if resource.extra_info.get('instance_state') in {'terminated', 'terminating'}: resource.resource_deleted_at = datetime.datetime.now() def filter(self, resource: CloudAsset) -> Tuple[bool, str]: passed, reason = super().filter(resource) if not passed: return False, reason if resource.product == AWSProduct.EC2 \ and resource.service_type == ServiceType.DEDICATED: return True, "" return False, "product or service_type mismatch" class AWSEBS(BaseLeakAnalyst): USEDBY_ALLOW_LIST = {UsedBy.CustomerTiDB, UsedBy.DBaaSControlPlaneShoot} def filter(self, resource: CloudAsset) -> Tuple[bool, str]: passed, reason = super().filter(resource) if not passed: return False, reason if resource.product == AWSProduct.EBS \ and resource.service_type == ServiceType.DEDICATED: return True, "" return False, "product or service_type mismatch" def leaked(self, resource: CloudAsset) -> LeakResult: # this EBS is not bound to any EC2 ebs_is_idle = False if resource.extra_info['ebs_state'] == 'available': ebs_is_idle = True usedby = resource.usedby if usedby == UsedBy.CustomerTiDB: alive, reason = self.md.cluster_alive(resource.cluster_id) if not alive: return LeakResult(True, f'the owner cluster is not alive: {reason}') if ebs_is_idle: # EBS is idle but cluster is not in 'Suspend' status suspend, reason = self.md.cluster_suspend(resource.cluster_id) if not suspend: return LeakResult(True, f'EBS state is "idle", but cluster is not suspend: {reason}') elif usedby == UsedBy.DBaaSControlPlaneShoot: if not resource.vpc_name and ebs_is_idle: return LeakResult(True, 'EBS state is "available"') alive, reason = self.md.network_alive(resource.project_id, resource.network_name, Provider.AWS, AWSProduct.EBS) if not alive: return LeakResult(True, f'the owner network is not alive: {reason}') if ebs_is_idle: return LeakResult(True, 'EBS state is "available"') return LeakResult(False, reason) class AWSELB(BaseLeakAnalyst): USEDBY_ALLOW_LIST = {UsedBy.CustomerTiDB, UsedBy.DBaaSControlPlaneShoot} def filter(self, resource: CloudAsset) -> Tuple[bool, str]: passed, reason = super().filter(resource) if not passed: return False, reason if resource.product in {AWSProduct.ELB, AWSProduct.CLB, AWSProduct.NLB} \ and resource.service_type == ServiceType.DEDICATED: return True, "" return False, "product or service_type mismatch" class AWSNAT(BaseLeakAnalyst): COMPONENT_IGNORE_LIST = { 'serverless', 'devtier-control-plane', UsedBy.DBaaSControlPlaneSeed, UsedBy.DBaaSControlPlaneBase, } USEDBY_ALLOW_LIST = { UsedBy.DBaaSControlPlaneShoot, } def extra_analyze(self, resource: CloudAsset, data: dict): super().extra_analyze(resource, data) if resource.shoot_id: shoot = self.md.get_shoot(resource.env, resource.shoot_id) if shoot: # some NATs used by gardener network have invalid labels, for example, nat-088de4a4d79837838 if resource.project_id == 'default': resource.tenant_id = shoot.tenant resource.project_id = shoot.project # some NATs used by gardener network have wrong 'usedby', for example, nat-0939cb912c5a06dbe if resource.usedby in {"customer-tidb", }: resource.usedby = UsedBy.DBaaSControlPlaneShoot def filter(self, resource: CloudAsset) -> Tuple[bool, str]: passed, reason = super().filter(resource) if not passed: return False, reason if resource.product == AWSProduct.NAT \ and resource.service_type == ServiceType.DEDICATED \ and resource.component not in self.COMPONENT_IGNORE_LIST: return True, "" return False, "product, service_type or component mismatch" class AWSEKS(BaseLeakAnalyst): COMPONENT_IGNORE_LIST = { 'serverless', 'devtier-control-plane', UsedBy.DBaaSControlPlaneSeed, UsedBy.DBaaSControlPlaneBase, } USEDBY_ALLOW_LIST = { UsedBy.DBaaSControlPlaneShoot, } def filter(self, resource: CloudAsset) -> Tuple[bool, str]: passed, reason = super().filter(resource) if not passed: return False, reason if resource.product == AWSProduct.EKS \ and resource.service_type == ServiceType.DEDICATED \ and resource.component not in self.COMPONENT_IGNORE_LIST: return True, "" return False, "product, service_type or component mismatch" class AWSASG(BaseLeakAnalyst): COMPONENT_IGNORE_LIST = { 'serverless', 'devtier-control-plane', UsedBy.DBaaSControlPlaneSeed, UsedBy.DBaaSControlPlaneBase, } USEDBY_ALLOW_LIST = { UsedBy.DBaaSControlPlaneShoot, UsedBy.CustomerTiDB, } def filter(self, resource: CloudAsset) -> Tuple[bool, str]: passed, reason = super().filter(resource) if not passed: return False, reason if resource.product == AWSProduct.ASG \ and resource.service_type == ServiceType.DEDICATED \ and resource.component not in self.COMPONENT_IGNORE_LIST: return True, "" return False, "product, service_type or component mismatch" class AWSNodeGroup(BaseLeakAnalyst): COMPONENT_IGNORE_LIST = { 'serverless', 'devtier-control-plane', UsedBy.DBaaSControlPlaneSeed, UsedBy.DBaaSControlPlaneBase, } USEDBY_ALLOW_LIST = { UsedBy.DBaaSControlPlaneShoot, UsedBy.CustomerTiDB, } def filter(self, resource: CloudAsset) -> Tuple[bool, str]: passed, reason = super().filter(resource) if not passed: return False, reason if resource.product == AWSProduct.NG \ and resource.service_type == ServiceType.DEDICATED \ and resource.component not in self.COMPONENT_IGNORE_LIST: return True, "" return False, "product, service_type or component mismatch" # AWS Security Group class AWSSG(BaseLeakAnalyst): COMPONENT_IGNORE_LIST = { 'serverless', 'devtier-control-plane', UsedBy.DBaaSControlPlaneSeed, UsedBy.DBaaSControlPlaneBase, } USEDBY_ALLOW_LIST = { UsedBy.DBaaSControlPlaneShoot, } def filter(self, resource: CloudAsset) -> Tuple[bool, str]: passed, reason = super().filter(resource) if not passed: return False, reason if resource.product == AWSProduct.SG \ and resource.service_type == ServiceType.DEDICATED \ and resource.component not in self.COMPONENT_IGNORE_LIST: return True, "" return False, "product, service_type or component mismatch" class AWSVPC(BaseLeakAnalyst): USEDBY_ALLOW_LIST = { UsedBy.DBaaSControlPlaneShoot, } def filter(self, resource: CloudAsset) -> Tuple[bool, str]: passed, reason = super().filter(resource) if not passed: return False, reason if resource.product == AWSProduct.VPC \ and resource.service_type == ServiceType.DEDICATED: return True, "" return False, "product or service_type mismatch" class GCPDisk(BaseLeakAnalyst): USEDBY_ALLOW_LIST = {UsedBy.CustomerTiDB, UsedBy.DBaaSControlPlaneShoot} def filter(self, resource: CloudAsset) -> Tuple[bool, str]: passed, reason = super().filter(resource) if not passed: return False, reason if resource.product == GCPProduct.Disk \ and resource.service_type == ServiceType.DEDICATED: return True, "" return False, "product or service_type mismatch" def leaked(self, resource: CloudAsset) -> LeakResult: # this EBS is not bound to any vm ebs_is_idle = False if resource.extra_info['ebs_state'] == 'available': ebs_is_idle = True usedby = resource.usedby if usedby == UsedBy.CustomerTiDB: alive, reason = self.md.cluster_alive(resource.cluster_id) if not alive: return LeakResult(True, f'the owner cluster is not alive: {reason}') if ebs_is_idle: # EBS is idle but cluster is not in 'Suspend' status suspend, reason = self.md.cluster_suspend(resource.cluster_id) if not suspend: return LeakResult(True, f'EBS state is "available", but cluster is not suspend: {reason}') elif usedby == UsedBy.DBaaSControlPlaneShoot: if not resource.vpc_name and ebs_is_idle: return LeakResult(True, 'EBS state is "available"') alive, reason = self.md.network_alive(resource.project_id, resource.network_name, resource.provider, resource.product) if not alive: return LeakResult(True, f'the owner network is not alive: {reason}') if ebs_is_idle: return LeakResult(True, 'EBS state is "available"') return LeakResult(False, '') class GCPVM(BaseLeakAnalyst): USEDBY_ALLOW_LIST = {UsedBy.CustomerTiDB, UsedBy.DBaaSControlPlaneShoot} def filter(self, resource: CloudAsset) -> Tuple[bool, str]: passed, reason = super().filter(resource) if not passed: return False, reason if resource.product == GCPProduct.VM \ and resource.service_type == ServiceType.DEDICATED: return True, "" return False, "product or service_type mismatch" class GCPLB(BaseLeakAnalyst): USEDBY_ALLOW_LIST = {UsedBy.CustomerTiDB, UsedBy.DBaaSControlPlaneShoot} def filter(self, resource: CloudAsset) -> Tuple[bool, str]: passed, reason = super().filter(resource) if not passed: return False, reason if resource.product == GCPProduct.LB \ and resource.service_type == ServiceType.DEDICATED: return True, "" return False, "product or service_type mismatch" class GCPNAT(BaseLeakAnalyst): USEDBY_ALLOW_LIST = {UsedBy.DBaaSControlPlaneShoot, } def filter(self, resource: CloudAsset) -> Tuple[bool, str]: passed, reason = super().filter(resource) if not passed: return False, reason if resource.product == GCPProduct.NAT \ and resource.service_type == ServiceType.DEDICATED \ and resource.usedby in {UsedBy.DBaaSControlPlaneShoot, ''}: return True, "" return False, "product, service_type or usedby mismatch" class GCPGKE(BaseLeakAnalyst): USEDBY_ALLOW_LIST = {UsedBy.DBaaSControlPlaneShoot, } def filter(self, resource: CloudAsset) -> Tuple[bool, str]: passed, reason = super().filter(resource) if not passed: return False, reason if resource.product == GCPProduct.GKE \ and resource.service_type == ServiceType.DEDICATED: return True, "" return False, "product or service_type mismatch" class GCPVPC(BaseLeakAnalyst): USEDBY_ALLOW_LIST = {UsedBy.DBaaSControlPlaneShoot, } def filter(self, resource: CloudAsset) -> Tuple[bool, str]: passed, reason = super().filter(resource) if not passed: return False, reason if resource.product == GCPProduct.VPC \ and resource.service_type == ServiceType.DEDICATED: return True, "" return False, "product or service_type mismatch" def build_uk4_resource(r: CloudAsset) -> str: return f'{r.env}/{r.provider}/{r.product}/{r.rid}' # got_or_empty get value from data by key, the value returned if key exists otherwise '' def got_or_default(data: dict, key: str, default: str = '') -> str: val = data.get(key) if val is None or val == 'null': return default else: return val def build_record_from_resource(provider: str, resource: dict, record: CloudAsset) -> CloudAsset: rid = got_or_default(resource, 'id') if not rid: rid = f"fakeid-{resource.get('name', '')}" if provider == Provider.AWS: record.resource_tags = resource['tags'] elif provider == Provider.GCP: record.resource_tags = resource['labels'] # use info in tags to overwrite common_cloud_resource_analyzer.sync_from_resource_labels(record, record.resource_tags) record.env = resource.get('env', '') record.provider = provider record.account_id = got_or_default(resource, 'account_id', '') record.region = got_or_default(resource, 'region', '') record.product = got_or_default(resource, 'product', '') record.rid = rid record.rname = got_or_default(resource, 'name', '') record.resource_created_at = datetime.datetime.fromtimestamp(resource.get('created_at', 0)) record.resource_deleted_at = None record.service_type = got_or_default(resource, 'service_type', '') record.usedby = got_or_default(resource, 'usedby', '') record.tenant_id = got_or_default(resource, 'tenant_id', '') record.project_id = got_or_default(resource, 'project_id', '') record.cluster_id = got_or_default(resource, 'cluster_id', '') record.component = got_or_default(resource, 'component', '') record.member = got_or_default(resource, 'member', '') record.vpc_name = got_or_default(resource, 'vpc_name', '') record.vpc_id = got_or_default(resource, 'vpc_id', '') record.vpc_type = got_or_default(resource, 'vpc_type', '') record.shoot_id = got_or_default(resource, 'shoot_id', '') record.k8s_name = got_or_default(resource, 'k8s', '') record.network_namespace = got_or_default(resource, 'network_namespace') record.network_name = got_or_default(resource, 'network_name') record.amend() # set extra_info if provider == Provider.AWS: if got_or_default(resource, 'billing_key'): util.set_if_empty(record.extra_info, 'arn', resource['billing_key']) if record.product == AWSProduct.EBS: record.extra_info['ebs_type'] = got_or_default(resource, 'ebs_type') record.extra_info['ebs_state'] = got_or_default(resource, 'ebs_state') record.extra_info['ebs_pvc_name'] = got_or_default(resource, 'ebs_pvc_name') record.extra_info['ebs_ec2_mount'] = got_or_default(resource, 'ebs_ec2_mount') record.extra_info['ebs_size'] = got_or_default(resource, 'ebs_size') record.extra_info['ebs_iops'] = got_or_default(resource, 'ebs_iops') record.extra_info['ebs_throughput'] = got_or_default(resource, 'ebs_throughput') elif record.product == AWSProduct.EC2: record.extra_info['instance_type'] = got_or_default(resource, 'ec2_instance_type') record.extra_info['instance_state'] = got_or_default(resource, 'ec2_instance_state') record.extra_info['hostname'] = got_or_default(resource, 'hostname') elif record.product in {AWSProduct.CLB, AWSProduct.NLB}: record.extra_info['elb_service_name'] = got_or_default(resource, 'elb_service_name') elif record.product == AWSProduct.NAT: record.extra_info['public_ip'] = got_or_default(resource, 'nat_public_ip') record.extra_info['eip_allocation_id'] = got_or_default(resource, 'eip_allocation_id') elif record.product == AWSProduct.VPC: record.extra_info['cidr_block'] = got_or_default(resource, 'cidr_block') elif provider == Provider.GCP: if got_or_default(resource, 'zone'): record.extra_info['zone'] = got_or_default(resource, 'zone') if record.product == GCPProduct.VM: record.extra_info['instance_type'] = got_or_default(resource, 'ec2_instance_type') record.extra_info['instance_state'] = got_or_default(resource, 'ec2_instance_state') record.extra_info['hostname'] = got_or_default(resource, 'name') elif record.product == GCPProduct.Disk: record.extra_info['ebs_state'] = got_or_default(resource, 'ebs_state') record.extra_info['ebs_size'] = got_or_default(resource, 'ebs_size') record.extra_info['ebs_pvc_name'] = got_or_default(resource, 'ebs_pvc_name') record.extra_info['ebs_ec2_mount'] = got_or_default(resource, 'ebs_ec2_mount') elif record.product == GCPProduct.LB: record.extra_info['ip_address'] = got_or_default(resource, 'ip_address') record.extra_info['port_range'] = got_or_default(resource, 'port_range') record.extra_info['lb_scheme'] = got_or_default(resource, 'lb_scheme') record.extra_info['psc_connection_id'] = got_or_default(resource, 'psc_connection_id') elif record.product == GCPProduct.NAT: # fixme(chenfei): currently, we haven't parse NAT IP for GCP record.extra_info['public_ip'] = got_or_default(resource, 'nat_public_ip') elif record.product == GCPProduct.VPC: record.extra_info['cidr_block'] = got_or_default(resource, 'cidr_block') record.extra_info.update(resource.get('extra_info', {})) return record def build_uk_for_resource(provider: str, resource: dict) -> str: rid = got_or_default(resource, 'id') if not rid: rid = f"fakeid-{resource.get('name', '')}" return f"{resource['env']}/{provider}/{resource['product']}/{rid}" def mark_resource_as_deleted_if_need(uk2resource: dict[str, CloudAsset], provider: str, resources: list[dict]): existing = {uk for uk, item in uk2resource.items() if item.provider == provider} current = {build_uk_for_resource(provider, resource) for resource in resources} # these resource are deleted records = [] for uk in (existing - current): record = uk2resource[uk] # skip if it's already deleted if record.resource_deleted_at: continue record.resource_deleted_at = datetime.datetime.now() records.append(record) batch_save(records) def append_if_changed(records: list, origin: CloudAsset, r: CloudAsset): r.resource_created_at = util.round_seconds(r.resource_created_at) # to avoid useless diff if origin.resource_created_at == '0000-00-00 00:00:00': origin.resource_created_at = ZERO_DATETIME origin_dict = model_to_dict(origin) record_dict = model_to_dict(r) diff = list(dictdiffer.diff(origin_dict, record_dict)) if diff: logger.info(f'asset changed, {r.env}/{r.provider}/{r.product}/{r.id}, diff={diff}') records.append(r) def build4provider(provider: str, resources: list[dict], uk2asset: dict[str, CloudAsset], product2analyzer: dict[str:BaseLeakAnalyst], options): mark_resource_as_deleted_if_need(uk2asset, provider, resources) records: list[CloudAsset] = [] for resource in resources: uk = build_uk_for_resource(provider, resource) origin = uk2asset.get(uk, CloudAsset()) record = deepcopy(origin) build_record_from_resource(provider, resource, record) analyst: BaseLeakAnalyst = product2analyzer.get(f'{provider}/{record.product}') leaked_result = None if analyst is None: leaked_result = LeakResult() else: analyst.extra_analyze(record, resource) skip, reason = analyst.filter(record) if not skip: leaked_result = LeakResult(False, f'skipped by filter: {reason}') else: missing_required_fields = analyst.missing_required_fields(record) if missing_required_fields: record.status = 'unknown' record.reason = 'miss required fields: ' + ','.join(missing_required_fields) else: leaked_result = analyst.leaked(record) # set record by LeakResult if leaked_result is not None: if record.status in ('leaked', 'cleaned'): # change status from 'leaked', 'cleaned' to other status is prohibited pass elif leaked_result.leaked: record.status = 'may-leaked' else: record.status = 'in-use' record.reason = leaked_result.reason append_if_changed(records, origin, record) batch_save(records, options) @timing def batch_save(records: list[CloudAsset], options=None): # Insert rows 100 at a time. logger.info(f'try to batch save {len(records)} records') if options is not None and options.dryrun: logger.info('skip for dryrun mode') return now = datetime.datetime.now() for each in records: each.updated_at = now idx = 0 for batch in peewee.chunked([model_to_dict(each) for each in records], 100): logger.info(f'batch {idx} save {len(batch)} records') idx += 1 with db.atomic(): try: CloudAsset.insert_many(batch).on_conflict(action='REPLACE').execute() pass except peewee.IntegrityError as e: logger.warning(f"save failed: {e}, continue") except Exception as e: logger.warning(f"save failed: {e}, continue") raise if __name__ == '__main__': parser = argparse.ArgumentParser( prog='resource_leak_analyzer', description='analyzer for cloud resource leak') parser.add_argument('--dryrun', action='store_true') # positional argument options = parser.parse_args() md = metadata.build_metadata() product2analyzer: dict[str:BaseLeakAnalyst] = { "aws/ebs": AWSEBS(md), "aws/ec2": AWSEC2(md), "aws/clb": AWSELB(md), "aws/nlb": AWSELB(md), "aws/nat": AWSNAT(md), "aws/eks": AWSEKS(md), "aws/asg": AWSASG(md), "aws/node_group": AWSNodeGroup(md), "aws/security_group": AWSSG(md), "aws/vpc": AWSVPC(md), "gcp/disk": GCPDisk(md), "gcp/vm": GCPVM(md), "gcp/lb": GCPLB(md), "gcp/nat": GCPNAT(md), "gcp/gke": GCPGKE(md), "gcp/vpc": GCPVPC(md), } init_db(create_db_instance()) resources_in_db = CloudAsset.select() uk2resource = {} for each in resources_in_db: uk2resource[build_uk4_resource(each)] = each aws_resources = json.load(open(settings.AWS_ASSET_SNAPSHOT_ANALYSIS_RESULT_FILE, 'r')) build4provider('aws', aws_resources, uk2resource, product2analyzer, options) gcp_resources = json.load(open(settings.GCP_ASSET_SNAPSHOT_ANALYSIS_RESULT_FILE, 'r')) build4provider('gcp', gcp_resources, uk2resource, product2analyzer, options)