From bb939a3a240174cb0375f64715dfe3b111644533 Mon Sep 17 00:00:00 2001 From: davidgiga1993 Date: Thu, 22 Jun 2023 09:12:35 +0200 Subject: [PATCH] feat: cross configmap state movement --- octoploy/octoploy.py | 26 +++++++------- octoploy/state/StateMover.py | 64 +++++++++++++++++++++++++++++++++ octoploy/state/StateTracking.py | 32 +++++++++-------- 3 files changed, 95 insertions(+), 27 deletions(-) create mode 100644 octoploy/state/StateMover.py diff --git a/octoploy/octoploy.py b/octoploy/octoploy.py index b9ed249..dbbfc2d 100644 --- a/octoploy/octoploy.py +++ b/octoploy/octoploy.py @@ -6,6 +6,7 @@ from octoploy.config.Config import RootConfig, RunMode from octoploy.deploy.AppDeploy import AppDeployment from octoploy.processing.DecryptionProcessor import DecryptionProcessor +from octoploy.state.StateMover import StateMover from octoploy.utils.Encryption import YmlEncrypter from octoploy.utils.Log import Log @@ -119,16 +120,13 @@ def list_state(args): def move_state(args): - run_mode = RunMode() source = args.items[0] dest = args.items[1] + target_cm = args.to root_config = load_project(args.config_dir) - root_config.initialize_state(run_mode) - - state = root_config.get_state() - state.move(source, dest) - root_config.persist_state(run_mode) + mover = StateMover(root_config) + mover.move(source, dest, dest_configmap=target_cm) def main(): @@ -155,14 +153,16 @@ def main(): state_list_parser = state_subparsers.add_parser('list') state_list_parser.set_defaults(func=list_state) - state_list_parser = state_subparsers.add_parser('mv') - state_list_parser.set_defaults(func=move_state) + state_mv_parser = state_subparsers.add_parser('mv') + state_mv_parser.set_defaults(func=move_state) - state_list_parser.add_argument('items', help='Source and destination.\n' - 'The format for an object is: octoployName/Namespace/k8sFqn\n' - 'For example: "my-old-app/Namespace2/k8sFqn" ' - '"my-new-app/Namespace2/k8sFqn"', nargs=2) - state_parser.set_defaults(func=move_state) + state_mv_parser.add_argument('items', help='Source and destination.\n' + 'The format for an object is: octoployName/Namespace/k8sFqn\n' + 'For example: "my-old-app/Namespace2/k8sFqn" ' + '"my-new-app/Namespace2/k8sFqn"', nargs=2) + state_mv_parser.add_argument('--to', dest='to', help='Configmap where the state should be moved to (optional).' + 'Format: configmapSuffix|namespace/configmapSuffix') + state_mv_parser.set_defaults(func=move_state) encrypt_parser = subparsers.add_parser('encrypt', help='Encrypts k8s secrets objects') encrypt_parser.add_argument('file', help='Yml file to be encrypted', nargs=1) diff --git a/octoploy/state/StateMover.py b/octoploy/state/StateMover.py new file mode 100644 index 0000000..a5f13a5 --- /dev/null +++ b/octoploy/state/StateMover.py @@ -0,0 +1,64 @@ +from typing import Optional, Tuple + +from octoploy.config.Config import RootConfig, RunMode +from octoploy.state.StateTracking import StateTracking +from octoploy.utils.Log import Log + + +class StateMover(Log): + def __init__(self, source_config: RootConfig): + super().__init__(__name__) + self._sourceConfig = source_config + + def move(self, source: str, dest: str, dest_configmap: Optional[str]): + """ + Moves the state from the given source to the given destination + :param source: Source + :param dest: Destination + :param dest_configmap: Different configmap which should receive the state + """ + if source.count('/') != dest.count('/'): + raise ValueError('Source and destination point to different path depths') + + run_mode = RunMode() + self._sourceConfig.initialize_state(run_mode) + source_state = self._sourceConfig.get_state() + + items_to_be_moved = source_state.get_items(source) + for item in items_to_be_moved: + key = item.get_key() + target = key.replace(source, dest) + self.log.info(f'Moving {key} to {target}') + item.update_from_key(target) + source_state.remove_key(key) + + target_state = source_state + target_namespace = self._sourceConfig.get_namespace_name() + if dest_configmap is not None: + target_state, target_namespace = self._get_state_from_cm(dest_configmap) + + for item in items_to_be_moved: + target_state.add(item) + + if len(items_to_be_moved) == 0: + self.log.warning('No items moved') + return + + # First persist the target to prevent data loss on error + if target_state != source_state: + target_state.store(target_namespace) + source_state.store(self._sourceConfig.get_namespace_name()) + + def _get_state_from_cm(self, configmap: str) -> Tuple[StateTracking, str]: + namespace = self._sourceConfig.get_namespace_name() + segments = configmap.split('/', 1) + if len(segments) == 2: + namespace = segments[0] + cm_suffix = segments[1] + else: + cm_suffix = segments[0] + + api = self._sourceConfig.create_api() + state = StateTracking(api, name_suffix=cm_suffix) + state.restore(namespace) + return state, namespace diff --git a/octoploy/state/StateTracking.py b/octoploy/state/StateTracking.py index b362b34..bf7a70a 100644 --- a/octoploy/state/StateTracking.py +++ b/octoploy/state/StateTracking.py @@ -111,21 +111,28 @@ def store(self, namespace: str): yml = YmlWriter.dump(data) self._k8s_api.apply(yml, namespace=namespace) - def move(self, source: str, dest: str): - if source.count('/') != dest.count('/'): - raise ValueError('Source and destination point to different path depths') + def add(self, object_state: ObjectState): + self._state[object_state.get_key()] = object_state + def remove(self, object_state: ObjectState): + del self._state[object_state.get_key()] + + def remove_key(self, key: str): + del self._state[key] + + def get_items(self, prefix: str) -> List[ObjectState]: + """ + Returns all state items which start with the given prefix + :param prefix: Prefix + :return: Items + """ + items = [] for value in list(self._state.values()): key = value.get_key() - if not key.startswith(source): + if not key.startswith(prefix): continue - target = key.replace(source, dest) - self.log.info(f'Moving {key} to {target}') - value.update_from_key(target) - - if key in self._state: - del self._state[key] - self._state[target] = value + items.append(value) + return items def get_not_visited(self, context: str) -> List[ObjectState]: """ @@ -172,9 +179,6 @@ def visit_only(self, context_name: str, k8s_object): if existing_state is not None: existing_state.visited = True - def remove(self, object_state: ObjectState): - del self._state[object_state.get_key()] - def print(self): self.log.info(f'State content of ConfigMap {self._cm_name}') for key, value in self._state.items():