Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,21 @@ RUN git clone --depth=1 https://github.com/RedHatQE/cloudwash.git && \

RUN /bin/bash -c 'cd ${CLOUDWASH_DIR}; for conffile in conf/*.yaml.template; do cp -- "$conffile" "${conffile%.yaml.template}.yaml"; done'

# Install openshift-install CLI for OCP cluster cleanup
RUN curl -kf https://mirror.openshift.com/pub/openshift-v4/x86_64/clients/ocp/stable/openshift-install-linux.tar.gz \
-o openshift-install-linux.tar.gz && \
tar zxf openshift-install-linux.tar.gz && \
chmod +x openshift-install

# adding .profile to environment variables, so it will be kept between shell sessions
RUN echo "source ${APP_ROOT}/.profile" >> ${APP_ROOT}/bin/activate && touch ${APP_ROOT}/.profile

# arbitrary UID handling starting from virtualenv directory for pip permissions
USER 0
RUN fix-permissions ${APP_ROOT} -P && \
git config --global --add safe.directory ${CLOUDWASH_DIR}
RUN cp openshift-install /usr/local/bin/ && \
rm -f openshift-install openshift-install-linux.tar.gz
USER 1001
WORKDIR "${CLOUDWASH_DIR}"

Expand Down
11 changes: 11 additions & 0 deletions cloudwash/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,14 @@
gce_data = ['VMS', 'NICS', 'DISCS']
vmware_data = ['VMS', 'NICS', 'DISCS']
container_data = ['CONTAINERS']

OCP_TAG_SUBSTR = "kubernetes.io/cluster/"
CLUSTER_NAME_TAGS = [
"clusterName",
"api.openshift.com/name",
]
CLUSTER_ID_TAGS = [
"openshiftClusterID",
"api.openshift.com/id",
]
CLUSTER_EXP_DATE_TAG = "expirationDate"
200 changes: 166 additions & 34 deletions cloudwash/entities/resources/ocps.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,92 @@
import tempfile

from cloudwash.config import settings
from cloudwash.constants import CLUSTER_EXP_DATE_TAG
from cloudwash.constants import CLUSTER_ID_TAGS
from cloudwash.constants import CLUSTER_NAME_TAGS
from cloudwash.constants import OCP_TAG_SUBSTR
from cloudwash.entities.resources.base import OCPsCleanup
from cloudwash.utils import calculate_time_threshold
from cloudwash.logger import logger
from cloudwash.utils import are_resources_older_than
from cloudwash.utils import check_installer_exists
from cloudwash.utils import destroy_ocp_cluster
from cloudwash.utils import dry_data
from cloudwash.utils import filter_resources_by_time_modified
from cloudwash.utils import group_ocps_by_cluster
from cloudwash.utils import OCP_TAG_SUBSTR
from cloudwash.utils import write_metadata_file


class LeftoverAWSOcp:
"""Represents an OCP cluster with its collected metadata and filtered AWS resources."""

def __init__(self, infra_id: str, region: str):
self.infra_id = infra_id
self.region = region
self.associated_resources = {"Resources": [], "Instances": []}
self._cluster_name = ""
self._cluster_id = ""
self._expiration_date = ""

def __repr__(self):
instances = len(self.associated_resources.get("Instances", []))
resources = len(self.associated_resources.get("Resources", []))
return (
f"LeftoverAWSOcp({self.infra_id}, region={self.region}, "
f"instances={instances}, resources={resources})"
)

def _collect_all_tags(self) -> dict:
"""Build a merged tag dict from all associated resources (first value wins)."""
tags = {}
for resources_list in self.associated_resources.values():
for resource in resources_list:
for tag in resource.get_tags():
key = tag.get("Key", "")
if key and key not in tags:
tags[key] = tag.get("Value", "")
return tags

def get_cluster_info(self):
"""Extract cluster name, ID, and expiration date from resource tags."""
tags = self._collect_all_tags()
if not self._expiration_date:
self._expiration_date = tags.get(CLUSTER_EXP_DATE_TAG, "")
if not self._cluster_name:
for name_tag in CLUSTER_NAME_TAGS:
if name_tag in tags:
self._cluster_name = tags[name_tag]
break
if not self._cluster_id:
for id_tag in CLUSTER_ID_TAGS:
if id_tag in tags:
self._cluster_id = tags[id_tag]
break

def get_cluster_metadata(self) -> dict:
"""Build the metadata dict required by openshift-install for cluster teardown."""
infra_id = self.infra_id
cluster_name = self._cluster_name or infra_id
cluster_id = self._cluster_id or infra_id

logger.info(f"\nPreparing metadata for cluster: {infra_id}")
return {
"clusterName": cluster_name,
"clusterID": cluster_id,
"infraID": infra_id,
"aws": {
"region": self.region,
"identifier": [{f"{OCP_TAG_SUBSTR}{infra_id}": "owned"}],
},
}


class CleanOCPs(OCPsCleanup):
def __init__(self, client):
self.client = client
self._delete = []
def __init__(self):
self._delete = {"resources": {}, "clusters": []}
self._cluster_map = {}
self.list()

def _set_dry(self):
dry_data['OCPS']['delete'] = self._delete
dry_data['OCPS']['delete'] = self._delete["resources"]
dry_data['OCPS']['clusters'] = self._delete["clusters"]

def list(self):
pass
Expand All @@ -24,40 +96,100 @@ def remove(self):

def cleanup(self):
if not settings.dry_run:
self.remove()
check_installer_exists()
with tempfile.TemporaryDirectory() as tmpdir:
for cluster_name in self._delete["clusters"]:
cluster = self._cluster_map[cluster_name]
cluster.get_cluster_info()
metadata = cluster.get_cluster_metadata()
metadata_path = write_metadata_file(
cluster_metadata=metadata, cleanup_dir=tmpdir
)
destroy_ocp_cluster(
metadata_path=metadata_path,
cluster_name=cluster_name,
)


class CleanAWSOcps(CleanOCPs):
def __init__(self, client):
self.client = client
self.cleaning_region = self.client.cleaning_region
super().__init__()

def group_ocps_by_cluster(self, resources: list = None) -> dict:
"""Group AWS resources under their originating OCP clusters by infra ID.

:param list resources: AWS resources collected by region and SLA
:return: Dict mapping cluster infra IDs to LeftoverAWSOcp instances
"""
if resources is None:
resources = []
clusters_map = {}

for resource in resources:
for key in resource.get_tags(regex=OCP_TAG_SUBSTR):
cluster_infra_id = key.get("Key")
if OCP_TAG_SUBSTR in cluster_infra_id:
parts = cluster_infra_id.split(OCP_TAG_SUBSTR)
if len(parts) < 2 or not parts[1]:
continue
cluster_infra_id = parts[1]
if cluster_infra_id not in clusters_map:
clusters_map[cluster_infra_id] = LeftoverAWSOcp(
infra_id=cluster_infra_id, region=self.cleaning_region
)

if hasattr(resource, 'ec2_instance'):
clusters_map[cluster_infra_id].associated_resources["Instances"].append(
resource
)
else:
clusters_map[cluster_infra_id].associated_resources["Resources"].append(
resource
Comment thread
jyejare marked this conversation as resolved.
Resolved
)
return clusters_map

def _filter_deletable(self):
"""Apply SLA and exception filters to determine which clusters are deletable."""
sla = settings.aws.criteria.ocps.get("SLA")
exceptions = list(settings.aws.exceptions.get("OCPS", []))

for cluster_name, cluster in self._cluster_map.items():
if cluster_name in exceptions:
continue

instances = cluster.associated_resources.get("Instances", [])
resources = cluster.associated_resources.get("Resources", [])

if instances:
if are_resources_older_than(time_ref=sla, resources=instances):
self._delete["clusters"].append(cluster_name)
self._delete["resources"].update(self._resources_by_type(resources + instances))
elif resources:
if are_resources_older_than(time_ref=sla, resources=resources):
self._delete["clusters"].append(cluster_name)
self._delete["resources"].update(self._resources_by_type(resources))

@staticmethod
def _resources_by_type(resources: list) -> dict:
"""Group resource names by their resource_type."""
result = {}
for r in resources:
result.setdefault(r.resource_type, []).append(r.name)
return result

def list(self):
resources = []
time_threshold = calculate_time_threshold(time_ref=settings.aws.criteria.ocps.get("SLA"))

ocp_prefix = list(settings.aws.criteria.ocps.get("OCP_PREFIXES") or [""])
for prefix in ocp_prefix:
ocp_prefixes = list(settings.aws.criteria.ocps.get("OCP_PREFIXES") or [""])
for prefix in ocp_prefixes:
query = " ".join(
[f"tag.key:{OCP_TAG_SUBSTR}{prefix}*", f"region:{self.client.cleaning_region}"]
[f"tag.key:{OCP_TAG_SUBSTR}{prefix}*", f"region:{self.cleaning_region}"]
)
resources.extend(self.client.list_resources(query=query))

# Prepare resources to be filtered before deletion
cluster_map = group_ocps_by_cluster(resources=resources)
for cluster_name in cluster_map.keys():
cluster_resources = cluster_map[cluster_name].get("Resources")
instances = cluster_map[cluster_name].get("Instances")
self._cluster_map = self.group_ocps_by_cluster(resources=resources)
self._filter_deletable()

if instances:
# For resources with associated EC2 Instances, filter by Instances SLA
if not filter_resources_by_time_modified(
time_threshold,
resources=instances,
):
self._delete.extend(cluster_resources)
else:
# For resources with no associated EC2 Instances, identify as leftovers
self._delete.extend(
filter_resources_by_time_modified(time_threshold, resources=cluster_resources)
)

# Sort resources by type
self._delete = sorted(self._delete, key=lambda x: x.resource_type)
self._delete["clusters"] = sorted(self._delete["clusters"])
self._set_dry()
2 changes: 2 additions & 0 deletions cloudwash/providers/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ def cleanup(**kwargs):
# Emptying the dry data for previous region everytime
for items in data:
dry_data[items]['delete'] = []
if items == 'OCPS':
dry_data[items]['clusters'] = []
logger.info(f"\nResources from the region: {region}")
awscleanup.ocps.cleanup()
if is_dry_run:
Expand Down
Loading
Loading