Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate cray-product-catalog ConfigMap to multiple product ConfigMaps #292

Merged
merged 21 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[`catalog_delete.py`](cray_product_catalog/catalog_delete.py), because we need not retry 100
times when read ConfigMap fails for a product ConfigMap.
- CASM-4504: Added label "type=cray-product-catalog" to all cray-product-catalog related ConfigMaps
- Implemented migration of cray-product-catalog ConfigMap to multiple ConfigMaps as part of pre-upgrade steps

### Dependencies

Expand Down
4 changes: 2 additions & 2 deletions cray_product_catalog/catalog_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,10 @@ def create_config_map(api_instance, name, namespace):
api_instance.create_namespaced_config_map(
namespace=namespace, body=new_cm
)
LOGGER.debug("Created product ConfigMap %s/%s", namespace, name)
LOGGER.info("Created product ConfigMap %s/%s", namespace, name)
return True
except ApiException:
LOGGER.warning("Error calling create_namespaced_config_map")
LOGGER.error("Error calling create_namespaced_config_map")
return False


Expand Down
122 changes: 67 additions & 55 deletions cray_product_catalog/migration/config_map_data_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@
from cray_product_catalog.migration.kube_apis import KubernetesApi
from cray_product_catalog.constants import (
PRODUCT_CATALOG_CONFIG_MAP_NAME, PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE,
PRODUCT_CATALOG_CONFIG_MAP_LABEL, PRODUCT_CATALOG_CONFIG_MAP_REPLICA
PRODUCT_CATALOG_CONFIG_MAP_LABEL
)
from cray_product_catalog.migration import CONFIG_MAP_TEMP

LOGGER = logging.getLogger(__name__)

Expand All @@ -61,67 +62,29 @@ def create_product_config_maps(self, product_config_map_data_list):
product_name = list(product_data.keys())[0]
LOGGER.debug("Creating ConfigMap for product %s", product_name)
prod_cm_name = format_product_cm_name(PRODUCT_CATALOG_CONFIG_MAP_NAME, product_name)
try:
if self.k8s_obj.create_config_map(prod_cm_name, PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE, product_data,
PRODUCT_CATALOG_CONFIG_MAP_LABEL):
LOGGER.debug("Created product ConfigMap %s/%s", PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE, prod_cm_name)
else:
LOGGER.info("Calling rollback handler")
except ApiException:
LOGGER.error("Error calling create_namespaced_config_map, exiting with rollback action")
LOGGER.info("Calling rollback handler")

if not self.k8s_obj.create_config_map(prod_cm_name, PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE, product_data,
PRODUCT_CATALOG_CONFIG_MAP_LABEL):
LOGGER.info("Failed to create product ConfigMap %s/%s", PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE, prod_cm_name)
return False
LOGGER.info("Created product ConfigMap %s/%s", PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE, prod_cm_name)
return True

def create_temp_config_map(self, config_map_data):
"""Create temporary main ConfigMap `cray-product-catalog-temp`

Args:
config_map_data (dict): Data to be stored in the ConfigMap `cray-product-catalog-temp`
"""
try:
if self.k8s_obj.create_config_map(PRODUCT_CATALOG_CONFIG_MAP_REPLICA,
PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE,
config_map_data, PRODUCT_CATALOG_CONFIG_MAP_LABEL):
LOGGER.DEBUG("Created temp ConfigMap %s/%s",
PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE, PRODUCT_CATALOG_CONFIG_MAP_REPLICA)
else:
LOGGER.info("Create ConfigMap failed, Calling rollback handler")
except ApiException:
LOGGER.error("Error calling create_namespaced_config_map, exiting with rollback action")
LOGGER.info("Calling rollback handler")

def create_main_config_map(self):
"""Create main ConfigMap `cray-product-catalog` using the data in
`cray-product-catalog-temp`"""
while True:
try:
self.k8s_obj.rename_config_map(PRODUCT_CATALOG_CONFIG_MAP_REPLICA,
PRODUCT_CATALOG_CONFIG_MAP_NAME, PRODUCT_CATALOG_CONFIG_MAP_LABEL)
LOGGER.info("Migration successful")
break
except ApiException:
LOGGER.warning("Failed to rename, retrying...")

def read_config_map_data(self):
"""Read main ConfigMap data

Returns:
{Dictionary}: data reperesenting the ConfigMap output

"""
try:
response = self.k8s_obj.read_config_map(PRODUCT_CATALOG_CONFIG_MAP_NAME,
PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE)
if response:
return response.data
LOGGER.info("No data available so exiting...")
raise SystemExit(1)
except ApiException as err:
LOGGER.exception("Error calling read_namespaced_config_map")
if err.status == 404:
LOGGER.error("ConfigMap %s/%s doesn't exist, exiting data migration",
PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE, PRODUCT_CATALOG_CONFIG_MAP_NAME)
LOGGER.info("Calling rollback handler")
raise SystemExit(1)
if self.k8s_obj.create_config_map(CONFIG_MAP_TEMP,
PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE,
config_map_data, PRODUCT_CATALOG_CONFIG_MAP_LABEL):
LOGGER.info("Created temp ConfigMap %s/%s",
PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE, CONFIG_MAP_TEMP)
return True
LOGGER.error("Creating ConfigMap %s/%s failed", PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE, CONFIG_MAP_TEMP)
return False

def migrate_config_map_data(self, config_map_data):
"""Migrate cray-product-catalog ConfigMap data to multiple product ConfigMaps with
Expand Down Expand Up @@ -166,5 +129,54 @@ def migrate_config_map_data(self, config_map_data):
if main_versions_data:
config_map_data[product] = yaml.safe_dump( main_versions_data, default_flow_style=False)
else:
config_map_data[product] = ""
config_map_data[product] = ''
return config_map_data, product_config_map_data_list

def rename_config_map(self, rename_from, rename_to, namespace, label):
""" Renaming is actually deleting one Config Map and then updating the name of other Config Map and patch it.
:param str rename_from: Name of Config Map to rename
:param str rename_to: Name of Config Map to be renamed to
:param str namespace: namespace in which Config Map has to be updated
:param dict label: label of config map to be renamed
:return: bool, If Success True else False
"""

if not self.k8s_obj.delete_config_map(rename_to, namespace):
logging.error("Failed to delete ConfigMap %s", rename_to)
return False
attempt = 0
del_failed = False

while attempt < 10:
attempt += 1
response = self.k8s_obj.read_config_map(rename_from, namespace)
if not response:
LOGGER.error("Failed to read ConfigMap %s, retrying..", rename_from)
continue
cm_data = response.data

if self.k8s_obj.create_config_map(rename_to, namespace, cm_data, label):
if self.k8s_obj.delete_config_map(rename_from, namespace):
LOGGER.info("Renaming ConfigMap successful")
return True
else:
LOGGER.error("Failed to delete ConfigMap %s, retrying..", rename_from)
del_failed = True
break
else:
LOGGER.error("Failed to create ConfigMap %s, retrying..", rename_to)
continue
# Since only delete of backed up ConfigMap failed, retrying only delete operation
attempt = 0
if del_failed:
while attempt < 10:
lathanm marked this conversation as resolved.
Show resolved Hide resolved
attempt += 1
if self.k8s_obj.delete_config_map(rename_from, namespace):
break
else:
LOGGER.error("Failed to delete ConfigMap %s, retrying..", rename_from)
continue
# Returning success as migration is successful only backed up ConfigMap is not deleted.
LOGGER.info("Renaming ConfigMap successful")
return True
return False
9 changes: 5 additions & 4 deletions cray_product_catalog/migration/exit_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@
from re import fullmatch


from cray_product_catalog.migration import CONFIG_MAP_TEMP, CONFIG_MAP_NAMESPACE, CRAY_DATA_CATALOG_LABEL, \
from cray_product_catalog.migration import CONFIG_MAP_TEMP, CRAY_DATA_CATALOG_LABEL, \
PRODUCT_CONFIG_MAP_PATTERN
from cray_product_catalog.constants import PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE
from cray_product_catalog.migration.kube_apis import KubernetesApi

LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -69,7 +70,7 @@ def __get_all_created_product_config_maps(self) -> List:
cm_name = filter(_is_product_config_map,
self.k8api.list_config_map(
label=CRAY_DATA_CATALOG_LABEL,
namespace=CONFIG_MAP_NAMESPACE)
namespace=PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE)
)
return list(cm_name)

Expand All @@ -82,7 +83,7 @@ def rollback(self):
product_config_maps = self.__get_all_created_product_config_maps() # collecting product config map

LOGGER.info("deleting ConfigMap %s", CONFIG_MAP_TEMP) # attempting to delete temp config map
if not self.k8api.delete_config_map(name=CONFIG_MAP_TEMP, namespace=CONFIG_MAP_NAMESPACE):
if not self.k8api.delete_config_map(name=CONFIG_MAP_TEMP, namespace=PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE):
LOGGER.error("Error in deleting ConfigMap %s. Delete this manually along with these %s",
CONFIG_MAP_TEMP, product_config_maps)
return
Expand All @@ -91,7 +92,7 @@ def rollback(self):
non_deleted_product_config_maps = []
for config_map in product_config_maps:
LOGGER.debug("deleting Product ConfigMap %s", config_map)
if not self.k8api.delete_config_map(name=config_map, namespace=CONFIG_MAP_NAMESPACE):
if not self.k8api.delete_config_map(name=config_map, namespace=PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE):
non_deleted_product_config_maps.append(config_map)

if len(non_deleted_product_config_maps) > 0: # checking if any product config map is not deleted
Expand Down
50 changes: 24 additions & 26 deletions cray_product_catalog/migration/kube_apis.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def __init__(self):
self.kclient.rest_client.pool_manager.connection_pool_kw['retries'] = retry
self.api_instance = client.CoreV1Api(self.kclient)

def create_config_map(self, data, name, namespace, label):
def create_config_map(self, name, namespace, data, label):
"""Creates Config Map
:param dict data: Content of configmap
:param str name: config map name to be created
Expand Down Expand Up @@ -101,14 +101,36 @@ def list_config_map(self, namespace, label):
self.logger.info("Either label or namespace is empty, not reading config map.")
return None
try:
return self.api_instance.list_namespaced_config_map(namespace, label_selector=label)
return self.api_instance.list_namespaced_config_map(namespace, label_selector=label).items
except MaxRetryError as err:
self.logger.exception('MaxRetryError - Error: {0}'.format(err))
return None
except ApiException as err:
self.logger.exception('ApiException- Error:{0}'.format(err))
return None

def list_config_map_names(self, namespace, label):
""" Reads all the Config Map with certain label in particular namespace
:param str namespace: Value of namespace from where config map has to be listed
:param str label: string format of label "type=xyz"
:return: [str]
"""
cm_output = self.list_config_map(namespace, label)

list_cm_names = []

if not cm_output:
return list_cm_names

# parse the output to get only names
for cm in cm_output:
try:
list_cm_names.append(cm.metadata.name)
except Exception:
continue

return list_cm_names

def read_config_map(self, name, namespace):
"""Reads config Map based on provided name and namespace
:param Str name: name of ConfigMap to read
Expand Down Expand Up @@ -145,30 +167,6 @@ def delete_config_map(self, name, namespace):
self.logger.exception('ApiException- Error:{0}'.format(err))
return False

def rename_config_map(self, rename_from, rename_to, namespace, label):
""" Renaming is actually deleting one Config Map and then updating the name of other Config Map and patch it.
:param str rename_from: Name of Config Map to rename
:param str rename_to: Name of Config Map to be renamed to
:param str namespace: namespace in which Config Map has to be updated
:param dict label: label of config map to be renamed
:return: bool, If Success True else False
"""

self.delete_config_map(rename_to, namespace)

try:
response = self.api_instance.read_namespaced_config_map(rename_from, namespace)
except MaxRetryError as err:
self.logger.exception('MaxRetryError - Error: {0}'.format(err))
return False
except ApiException as err:
self.logger.exception("ApiException- Error:{0}".format(err))
return False
else:
self.create_config_map(response.data, rename_to, namespace, label)
self.delete_config_map(rename_from, namespace)
return True

def update_role_permission(self, name, namespace, action: str, is_grant: bool):
"""Updates specific role for permission
:param str name: name of role to be updated
Expand Down
49 changes: 42 additions & 7 deletions cray_product_catalog/migration/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,61 @@

import logging

from cray_product_catalog.migration import CONFIG_MAP_TEMP
from cray_product_catalog.migration.config_map_data_handler import ConfigMapDataHandler
from cray_product_catalog.constants import (
PRODUCT_CATALOG_CONFIG_MAP_NAME,
PRODUCT_CATALOG_CONFIG_MAP_REPLICA
PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE,
PRODUCT_CATALOG_CONFIG_MAP_LABEL
)
from cray_product_catalog.migration.exit_handler import ExitHandler

LOGGER = logging.getLogger(__name__)

def main():
"""Main function"""
LOGGER.info("Migrating %s ConfigMap data to multiple product ConfigMaps", PRODUCT_CATALOG_CONFIG_MAP_NAME)
config_map_obj = ConfigMapDataHandler()
config_map_data = config_map_obj.read_config_map_data()
main_config_map_data, product_config_map_data_list = config_map_obj.migrate_config_map_data(config_map_data)
config_map_obj.create_product_config_maps(product_config_map_data_list)
config_map_obj.create_temp_config_map(main_config_map_data)
exit_handler = ExitHandler()
response = config_map_obj.k8s_obj.read_config_map(
PRODUCT_CATALOG_CONFIG_MAP_NAME, PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE
)
if response and response.data:
config_map_data = response.data
else:
LOGGER.info("Error reading ConfigMap, exiting migration process...")
raise SystemExit(1)

try:
main_config_map_data, product_config_map_data_list = config_map_obj.migrate_config_map_data(config_map_data)
except Exception:
LOGGER.error("Failed to split ConfigMap Data, exiting migration process...")
raise SystemExit(1)

# Create ConfigMaps for each product with `component_versions` data
if not config_map_obj.create_product_config_maps(product_config_map_data_list):
LOGGER.info("Calling rollback handler...")
exit_handler.rollback()
raise SystemExit(1)
# Create temporary main ConfigMap with all data except `component_versions` for all products
if not config_map_obj.create_temp_config_map(main_config_map_data):
LOGGER.info("Calling rollback handler...")
exit_handler.rollback()
raise SystemExit(1)

LOGGER.info("Renaming %s ConfigMap name to %s ConfigMap",
PRODUCT_CATALOG_CONFIG_MAP_REPLICA, PRODUCT_CATALOG_CONFIG_MAP_NAME)
config_map_obj.create_main_config_map()
CONFIG_MAP_TEMP, PRODUCT_CATALOG_CONFIG_MAP_NAME)
#Creating main ConfigMap `cray-product-catalog` using the data in `cray-product-catalog-temp`
if config_map_obj.rename_config_map(
CONFIG_MAP_TEMP, PRODUCT_CATALOG_CONFIG_MAP_NAME,
PRODUCT_CATALOG_CONFIG_MAP_NAMESPACE, PRODUCT_CATALOG_CONFIG_MAP_LABEL
):
LOGGER.info("Migration successful")
else:
LOGGER.info("Renaming %s to %s ConfigMap failed, calling rollback handler...",
CONFIG_MAP_TEMP, PRODUCT_CATALOG_CONFIG_MAP_NAME)
exit_handler.rollback()
raise SystemExit(1)


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion cray_product_catalog/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def __init__(self, name=PRODUCT_CATALOG_CONFIG_MAP_NAME, namespace=PRODUCT_CATAL
str(p) for p in self.products if not p.is_valid
]
if invalid_products:
LOGGER.debug(
LOGGER.warning(
'The following products have product catalog data that is not valid against the expected schema: %s',
", ".join(invalid_products)
)
Expand Down
Loading