afaisman / Test

0 stars 0 forks source link

ns #10

Open afaisman opened 1 week ago

afaisman commented 1 week ago

import logging import time

from eks_hk.nsmanager.application.services.deployments.nsDeploymentManager import NSDeploymentManager from eks_hk.nsmanager.application.services.platform.aws_service import AWSService from eks_hk.nsmanager.application.services.platform.kubernetes_service import KubernetesService from eks_hk.nsmanager.application.utils.time_utils import TimeUtils

""" The NSController class manages the lifecycle and operations of NSDeployment instances. The class supports operations such as:

class NSController: LOGGER = logging.getLogger(name)

@staticmethod
def make_new() -> "NSController":
    controller = None
    try:
        controller = NSController(tick_counter_=0, deployment_names_=[], eks_deployments_list_=[],
                                  eks_deployment_names_list_=[], time_to_update_houskeeper_instance_sec_=60,
                                  is_initialized_=False)
    except Exception as e:
        NSController.LOGGER.error(f"Exception when creating HousekeeperController instance: {e}")

    return controller

def __init__(self, tick_counter_: int, deployment_names_: list, eks_deployments_list_: list,
             eks_deployment_names_list_: list, time_to_update_houskeeper_instance_sec_: int, is_initialized_: bool):
    self._tick_counter = tick_counter_
    self._deployment_names = deployment_names_
    self._eks_deployments_list = eks_deployments_list_
    self._eks_deployment_names_list = eks_deployment_names_list_
    self._time_to_update_houskeeper_instance_sec = time_to_update_houskeeper_instance_sec_
    self._is_initialized = is_initialized_

def initialize(self, ssm_client_, aws_service_: AWSService, kubernetes_service_: KubernetesService,
               nsdeployment_manager_: NSDeploymentManager) -> bool:
    self._ssm_client = ssm_client_
    self._aws_service = aws_service_
    self._kubernetes_service = kubernetes_service_
    self._ns_deployment_manager = nsdeployment_manager_

    ret_code = True
    try:
        self._is_initialized = False
        try:
            # Get the list of deployments in the current namespace
            self._eks_deployments_list = self._kubernetes_service.list_namespaced_deployment(
                namespace_=self._kubernetes_service.get_namespace())
            NSController.LOGGER.info(f"self._eks_deployments_list={self._eks_deployments_list}")

            self._eks_deployment_names_list = [deployment.metadata.name for deployment in
                                               self._eks_deployments_list.items]
        except Exception as e:
            NSController.LOGGER.error(f"Exception when calling AppsV1Api->list_namespaced_deployment: {e}")
            ret_code = False

        # --- Debug code begin if not in cluster, set some values for debugging
        if self._kubernetes_service._incluster is False:
            self._kubernetes_service._namespace = "p11-realtime-1"
            self._eks_deployment_names_list = ["transcribe-engine"]
            self._kubernetes_service._cluster_name = "p11"
            self._kubernetes_service._region_name = "us-east-1"
            ret_code = True
        # --- Debug code end

        for eks_deployment_name in self._eks_deployment_names_list:
            self._ns_deployment_manager.load_deployment(eks_deployment_name)
        if ret_code is True:
            self._is_initialized = True
    except Exception as e:
        NSController.LOGGER.error(f"Exception when calling AppsV1Api->list_namespaced_deployment: {e}")
        ret_code = False

    return ret_code

def time_to_refresh_housekeeper_deployment(self, housekeeper_deployment_):
    if housekeeper_deployment_ is None:
        return True
    time_diff = TimeUtils.get_est_now() - housekeeper_deployment_.time_created
    if time_diff.total_seconds() > self._time_to_update_houskeeper_instance_sec:
        return True

    return False

def set_updated_datum(self, housekeeper_deployment_):
    updateddatum = housekeeper_deployment_.get_datum(data_id_="updated")
    if updateddatum is not None:
        updateddatum.set_value(updateddatum.get_value() + 1)

# Update state of all managed deployments based on the current time
# Note: deployment's tick() returns tuple of booleans (Succeded, Data were updated)
def tick(self, current_time_, refresh_deployments=True):
    self._tick_counter = self._tick_counter + 1

    try:
        region = self._kubernetes_service.get_region()
        items = self._ns_deployment_manager.get_deployment_instances_list()  # to avoid 'dictionary changes size during iteration error
        for deployment_name, nsDeployment in items:
            curr_deployment = nsDeployment
            if nsDeployment and not nsDeployment.active: # "nsDeployment is not None" makes IntelliJ crazy
                continue

            if (refresh_deployments is not None) and self.time_to_refresh_housekeeper_deployment(nsDeployment):
                curr_deployment = self._ns_deployment_manager.load_deployment(deployment_name)

            data_updated = False
            if curr_deployment is not None:
                succeded, data_updated = curr_deployment.tick(current_time_)

            if data_updated is True:
                NSController.LOGGER.info(f"Data updated, saving to Parameter Store")
                self.set_updated_datum(nsDeployment)
                self._aws_service.save_data_to_parameterstore(nsDeployment=nsDeployment, region_=region)

        return True
    except Exception as e:
        NSController.LOGGER.error(f"Exception: {e}")
        return False

def main_loop(self):
    while True:
        NSController.LOGGER.info("{}".format("- - " * 10))
        NSController.LOGGER.info(f"Tick started\n")

        current_time = TimeUtils.get_est_now()
        self.tick(current_time)
        NSController.LOGGER.info(f"Tick ended\n")
        NSController.LOGGER.info("{}".format("- - " * 10))
        time.sleep(30)

#######
afaisman commented 1 week ago

import logging

from eks_hk.nsmanager.application.models.deployments.nsDeployment import NSDeployment

class TickEngine: """ TickEngine Class:

Purpose:
Manages and executes scheduled operations based on a ticking mechanism.
This engine evaluates and executes operations at specified intervals,
ensuring that actions are triggered at the correct times.

Main API:
- __init__(): Initializes the engine, setting it to active.
- do_schedule(nsDeployment_, current_time_): Schedules and executes operations
  based on the current time. Returns a tuple indicating success and if a data update occurred.
"""

LOGGER = logging.getLogger(__name__)

def __init__(self):
    self.ticker_active = True
    pass

# Note: do_schedule() returns tuple of booleans (Succeded, Executed, Data were updated)
def do_schedule(self, nsdeployment_: NSDeployment, current_time_):
    """
    Schedules and executes operations from the nsDeployment_ object at the given current_time_.

    Parameters:
    - nsDeployment_ : An object containing operations that may need to be executed.
    - current_time_ : The current time to evaluate which operations should be executed.

    Returns:
    - Tuple (success, update_required) where:
      - success (bool)
      - update_required (bool): Indicates if any data was updated during the operations,
        which may require serializing data to AWS.
    """
    try:
        #  Returns: (success, data_updated). data_updated is used by housekeepercontroller to decide if serializing tp the paramter store is needed
        update_required = False
        for operation in nsdeployment_.operations:
            succeeded, executed, data_updated = operation.execute(current_time_, nsdeployment_)
            if data_updated is True:
                update_required = True

        return True, update_required
    except Exception as e:
        NSDeployment.LOGGER.error(f"Exception: {e}")
        return False, False
afaisman commented 1 week ago

import datetime import logging from typing import Optional

import pytz import yaml

from eks_hk.nsmanager.application.models.operations.datum import Datum from eks_hk.nsmanager.application.utils.string_utils import StringUtils from eks_hk.nsmanager.application.utils.time_utils import TimeUtils

class NSData: """ Manages the data related to a specific network deployment. Provides mechanisms to serialize deployment data. The purpose of the class is to encapsulate and manage ata related to a specific deployment.

Main API:

afaisman commented 1 week ago

import datetime import logging from typing import Optional

import pytz import yaml

from eks_hk.nsmanager.application.models.deployments.nsData import NSData from eks_hk.nsmanager.application.models.operations.datum import Datum from eks_hk.nsmanager.application.models.operations.operation import Operation from eks_hk.nsmanager.application.utils.string_utils import StringUtils

add class level description

class NSDeployment: """ Manages Kubernetes deployment configurations. This class is designed to handle the lifecycle of Kubernetes deployment configurations, mainly watermarking, swcaling down and scaling up.

Main API:

afaisman commented 1 week ago

import logging

from eks_hk.nsmanager.application.utils.time_utils import TimeUtils

""" The Datum class encapsulates data records with identifiers, values, and timestamps. It supports:

class Datum: LOGGER = logging.getLogger(name)

def __init__(self, data_id, value, recorded_time):
    self.data_id = data_id
    self._value = value
    self.recorded_time = recorded_time

def to_dict(self):
    try:
        return {"data_id": self.data_id, "value": self._value, "recorded_time": self.recorded_time}
    except Exception as e:
        Datum.LOGGER.error(Datum.LOGGER, f"Exception: {e}")
        return None

def set_value(self, value):
    self._value = value
    current_time = TimeUtils.get_est_now()  # datetime.datetime.now(ZoneInfo("US/Eastern"))
    self.recorded_time = current_time

@classmethod
def from_dict(cls, data_dict):
    try:
        return cls(**data_dict)
    except Exception as e:
        Datum.LOGGER.error(f"Exception: {e}")
        return None

def get_value(self):
    return self._value
afaisman commented 1 week ago

import logging

from eks_hk.nsmanager.application.models.operations.scaleOperation import ScaleOperation

""" A subclass of ScaleOperation designed for immediate and unconditional scaling of Kubernetes pods. It is uuseful for debugging or testing scenarios where immediate pod scaling is required.

class ImmediateScaleOperation(ScaleOperation): LOGGER = logging.getLogger(name)

def __init__(self, id, type, scale_to):
    super().__init__(id, type, None, None, None)
    self.scale_to = scale_to

def is_relevant(self, current_time_):
    return True

# Returns tuple of booleans (Succeded, Executed, Data were updated)
def execute(self, current_time_, housekeeper_deployment_):
    try:
        if self.is_relevant(current_time_):
            res = False
            current_num_of_pods = -1
            replicas = self.scale_to
            if not housekeeper_deployment_.skip_kubernetes_scale:
                ImmediateScaleOperation.LOGGER.info(
                    f"Before scale_deployment {housekeeper_deployment_.name} to {replicas}")
                res, current_num_of_pods = self.scale_deployment(housekeeper_deployment_, replicas)
            else:
                ImmediateScaleOperation.LOGGER.info(
                    f"Deployment {housekeeper_deployment_.name} not scaling to {replicas} since skip_kubernetes_scale==True")

            if res:
                if current_num_of_pods != replicas:
                    self.report_execute(housekeeper_deployment_=housekeeper_deployment_,
                                        current_num_of_pods_=current_num_of_pods, target_num_of_pods=replicas,
                                        message_=f"Immediate scale: scaling deployment {housekeeper_deployment_.name} to {self.scale_to} pods.")
                else:
                    self.report_execute(housekeeper_deployment_=housekeeper_deployment_,
                                        current_num_of_pods_=current_num_of_pods, target_num_of_pods=replicas,
                                        message_=f"Immediate scale: not scaling (not need to scale?) deployment {housekeeper_deployment_.name} to {self.scale_to} pods.")
            else:
                self.report_execute(housekeeper_deployment_=housekeeper_deployment_,
                                    current_num_of_pods_=current_num_of_pods, target_num_of_pods=replicas,
                                    message_=f"Immediate Scaling error.")

            return True, True, False
        else:
            return True, False, False
    except Exception as e:
        ImmediateScaleOperation.LOGGER.error(f"Exception: {e}")
        return False, False, False

def to_dict(self):
    try:
        return {"id": self.id, "type": self.type, "scale_to": self.scale_to}
    except Exception as e:
        ImmediateScaleOperation.LOGGER.error(f"Exception: {e}")
        return None
afaisman commented 1 week ago

import logging

import yaml from kubernetes import client

from eks_hk.nsmanager.application.services.platform.aws_service import AWSService from eks_hk.nsmanager.application.services.platform.kubernetes_service import KubernetesService from eks_hk.nsmanager.application.utils.string_utils import StringUtils from eks_hk.nsmanager.application.utils.time_utils import TimeUtils, reset_to_full_hour, \ reset_to_previous_middle_of_hour, reset_to_half_hour, extract_minute_from_string, extract_minute_from_string_w_every

""" The Operation class manages operations with specific timing and target specifications. It supports:

class Operation: LOGGER = logging.getLogger(name)

def __init__(self, id, type, execution_time, target_percentage, time_span=None):
    self.id = id
    self.type = type
    self.execution_time = execution_time
    self.target_percentage = target_percentage
    self.time_span = 180

def initialize(self, ssm_client_, string_utils_: StringUtils, aws_service_: AWSService,
               kubernetes_service_: KubernetesService):
    self._ssm_client = ssm_client_
    self._string_utils = string_utils_
    self._aws_service = aws_service_
    self._kubernetes_service = kubernetes_service_

def to_dict(self):
    try:
        return {
            "id": self.id,
            "type": self.type,
            "execution_time": self.execution_time,
        }
    except Exception as e:
        Operation.LOGGER.error(f"Exception: {e}")
        return None

@classmethod
def from_dict(cls, data_dict):
    try:
        return cls(**data_dict)
    except Exception as e:
        Operation.LOGGER.error(f"Exception: {e}")
        return None

# Base method. If relevant (means current_time_ overlaps with the lifespan), them log.
# Returns tuple of booleans (Succeded, Executed, Data were updated)
def execute(self, current_time_, housekeeper_deployment_) -> (bool, bool, bool):
    try:
        if self.is_relevant(current_time_):
            return True, True, False
        else:
            return True, False, False
    except Exception as e:
        Operation.LOGGER.error(f"Exception: {e}")
        return False, False, False

# Returns True if the given operaiton is relevant == current_time_ belongs to the operation's live span
def is_relevant(self, current_time_) -> bool:
    try:
        if self.execution_time.lower().startswith("now"):
            return True

        if self.execution_time.lower() == "Hour".lower():
            adjusted_execution_time = str(reset_to_full_hour(current_time_))
        elif self.execution_time.lower() == "MiddleOfHour".lower():
            adjusted_execution_time = str(reset_to_previous_middle_of_hour(current_time_))
        elif self.execution_time.lower() == "HalfHour".lower():
            adjusted_execution_time = str(reset_to_half_hour(current_time_))
        elif not self.execution_time.lower().startswith("every_") and self.execution_time.lower().endswith("_min"):
            min = extract_minute_from_string(self.execution_time.lower())
            adj_t = current_time_.replace(minute=min)
            adjusted_execution_time = str(adj_t)
        elif self.execution_time.lower().startswith("every_") and self.execution_time.lower().endswith("_min"):
            every_x_min = extract_minute_from_string_w_every(self.execution_time.lower())
            minutes = current_time_.minute
            multiple = round(minutes / every_x_min) * every_x_min
            if multiple >= 60:
                multiple -= every_x_min
            adj_t = current_time_.replace(minute=multiple)
            adjusted_execution_time = str(adj_t)
        else:
            adjusted_execution_time = TimeUtils.adjust_pattern_tine_to_current_time(current_time_,
                                                                                    self.execution_time)
            if not adjusted_execution_time:
                return False

        ## the operation is still relevant if (now()~(current_time_) - self.execution_time < self.time_span)
        time_diff = TimeUtils.time_diff(current_time_, adjusted_execution_time)
        # if time_diff.total_seconds() > 0 and time_diff.total_seconds() <= self.time_span:
        if 0 < time_diff <= self.time_span:
            return True
        else:
            return False
    except Exception as e:
        Operation.LOGGER.error(f"Exception: {e}")
        return False

def __str__(self):
    dict_dump = self.to_dict()
    return yaml.dump(dict_dump, sort_keys=False, default_flow_style=False)

# Retrieves the number of pods for a given deployment.
def get_n_pods(self, housekeeper_deployment_) -> int:
    namespace = self._kubernetes_service.get_namespace()
    deployment_name = housekeeper_deployment_.name
    try:
        deployment = self._kubernetes_service.read_namespaced_deployment(deployment_name, namespace)
        Operation.LOGGER.info(
            f"Deployement {housekeeper_deployment_.name} currently has {deployment.spec.replicas} replicas")
        return deployment.spec.replicas
    except client.exceptions.ApiException as e:
        Operation.LOGGER.error("Kubernetes API error: " + str(e))
        return 0
    except Exception as e:
        Operation.LOGGER.error(f"Error: {str(e)}")
        return -1

def scale_deployment(self, housekeeper_deployment_, replicas) -> (bool, int):
    try:
        namespace = self._kubernetes_service.get_namespace()
        deployment_name = housekeeper_deployment_.name
        Operation.LOGGER.info(
            f"scale_deployment namespace={namespace} deplayment_name={deployment_name}, replicas={replicas}")

        deployment = self._kubernetes_service.read_namespaced_deployment(deployment_name, namespace)
        current_num_of_pods = deployment.spec.replicas
        Operation.LOGGER.info(f"Deployment {deployment_name} currently has {current_num_of_pods} replicas")
        if current_num_of_pods != replicas:
            deployment.spec.replicas = replicas
            self._kubernetes_service.replace_namespaced_deployment(deployment_name_=deployment_name,
                                                                   namespace_=namespace, body_=deployment)
            Operation.LOGGER.info("Deployment scaled.")
            return True, current_num_of_pods
        else:
            Operation.LOGGER.info("No need to scale")
            return True, current_num_of_pods

    except Exception as e:
        Operation.LOGGER.error("Error: " + str(e))
        return False
afaisman commented 1 week ago

import logging from typing import Union

from eks_hk.nsmanager.application.engine.tickEngine import TickEngine from eks_hk.nsmanager.application.models.deployments.nsDeployment import NSDeployment from eks_hk.nsmanager.application.services.deployments.nsDeploymentFactory import NSDeploymentFactory from eks_hk.nsmanager.application.services.platform.aws_service import AWSService from eks_hk.nsmanager.application.services.platform.kubernetes_service import KubernetesService from eks_hk.nsmanager.application.utils.log_event import log_housekeeper_event from eks_hk.nsmanager.application.utils.string_utils import StringUtils

class NSDeploymentManager: """ Manages NSDeployments which are specific deployment configurations within a Kubernetes and AWS environment.

This manager class integrates services to facilitate the creation, retrieval, and management of NSDeployment
instances. It handles operations such as loading deployments
from AWS Parameter Store and maintaining a map of Kubernetes deployment names to NSDeployment instances.

Methods:
    __init__: Initializes the NSDeploymentManager with necessary service clients and utilities.
    get_deployment_instances_list: Returns a list of tuples containing the Kubernetes deployment names and their corresponding NSDeployment instances.
    load_deployment: Attempts to load a deployment by its name, resolving its configuration via the AWS Parameter Store and updates the internal map with the deployment instance.

    In case of failure during the deployment loading process, logs the error and the operation's result, and returns None indicating that the deployment could not be loaded or resolved.
"""

LOGGER = logging.getLogger(__name__)

def __init__(self, ssm_client_, ns_deployment_factory_: NSDeploymentFactory, string_utils_: StringUtils,
             aws_service_: AWSService, kubernetes_service_: KubernetesService, tick_engine_: TickEngine):
    self._map_kubedeploymentnames_to_nsdeploymentinstances_dict = {}
    self._ns_deployment_factory = ns_deployment_factory_
    self._ssm_client = ssm_client_
    self._string_utils = string_utils_
    self._aws_service = aws_service_
    self._kubernetes_service = kubernetes_service_
    self._tick_engine = tick_engine_

def get_deployment_instances_list(self):
    return list(self._map_kubedeploymentnames_to_nsdeploymentinstances_dict.items())

def load_deployment(self, deployment_name_) -> Union[None, NSDeployment]:
    try:
        prefix = self._aws_service.get_parameter_store_prefix()
        region = self._kubernetes_service.get_region()
        cluster = self._kubernetes_service.get_cluster_name()
        namespace = self._kubernetes_service.get_namespace()
        # !!!!!!!!!!!! pls. move all these variables into a config file
        VERSION = "1.0.0"

        NSDeploymentManager.LOGGER.info(
            f"load_deployment {prefix}/{region}/{VERSION}/{cluster}/{namespace}/{deployment_name_}")
        parameter_store_key = f"{prefix}/{region}/{VERSION}/{cluster}/{namespace}/{deployment_name_}"

        deployment = self._ns_deployment_factory.make_nsdeployment(parameter_store_key,
                                                                   ssm_client_=self._ssm_client,
                                                                   string_utils_=self._string_utils,
                                                                   kubernetes_service_=self._kubernetes_service,
                                                                   aws_service_=self._aws_service,
                                                                   tick_engine_=self._tick_engine)
        if deployment is None:
            log_housekeeper_event(event="load_deployment", operation="", eventRetCode_=0,
                                  deployment_=deployment_name_, target_percentage_="", current_num_of_pods_="",
                                  target_num_of_pods_="", execution_time_=0, message_="Could not load")
            return None

        log_housekeeper_event(event="load_deployment", operation="", eventRetCode_=0, deployment_=deployment_name_,
                              target_percentage_="", current_num_of_pods_="", target_num_of_pods_="",
                              execution_time_=0,
                              message_=f"Loading deployment {deployment_name_} in {namespace} {cluster} {region} ")

        deployment.deployment_name = deployment_name_
        deployment.housekeeper_deployment = deployment
        NSDeploymentManager.LOGGER.info("{}".format("- - " * 10))
        NSDeploymentManager.LOGGER.info(f"Deployment from {parameter_store_key} was resolved as:\n")
        NSDeploymentManager.LOGGER.info(deployment)
        NSDeploymentManager.LOGGER.info("{}".format("- - " * 10))
        self._map_kubedeploymentnames_to_nsdeploymentinstances_dict[deployment_name_] = deployment
        return deployment
    except Exception as e:
        NSDeploymentManager.LOGGER.error(f"Exception: {e}")
        self._map_kubedeploymentnames_to_nsdeploymentinstances_dict[deployment_name_] = None
        return None
afaisman commented 1 week ago

import inspect import logging from typing import Optional, Union

import yaml from botocore.exceptions import ClientError

from eks_hk.nsmanager.application.engine.tickEngine import TickEngine from eks_hk.nsmanager.application.models.deployments.nsDeployment import NSDeployment from eks_hk.nsmanager.application.models.operations.datum import Datum from eks_hk.nsmanager.application.models.operations.defineTransactionIdPrefixOperation import \ DefineTransactionIdPrefixOperation from eks_hk.nsmanager.application.models.operations.immediateScaleOperation import ImmediateScaleOperation from eks_hk.nsmanager.application.models.operations.operation import Operation from eks_hk.nsmanager.application.models.operations.scaleOperation import ScaleOperation from eks_hk.nsmanager.application.models.operations.transactionBeginOperation import TransactionBeginOperation from eks_hk.nsmanager.application.models.operations.transactionEndOperation import TransactionEndOperation from eks_hk.nsmanager.application.models.operations.watcherOperation import WatcherOperation from eks_hk.nsmanager.application.models.operations.watermarkOperation import WatermarkOperation from eks_hk.nsmanager.application.services.platform.aws_service import AWSService from eks_hk.nsmanager.application.services.platform.kubernetes_service import KubernetesService from eks_hk.nsmanager.application.utils.string_utils import StringUtils

class NSDeploymentFactory: """ The NSDeploymentFactory class provides method for creating NSDeployment instances based on configuration data stored in AWS SSM Parameter Store. It encapsulates the logic for loading deployment configurations either from JSON or YAML formats, handling exceptions, and logging pertinent information throughout the process. The class supports dynamic deployment creation. """

LOGGER = logging.getLogger(__name__)

@staticmethod
def make_new() -> "NSDeploymentFactory":
    nseploymentFactory = None
    try:
        nseploymentFactory = NSDeploymentFactory()
    except Exception as e:
        NSDeploymentFactory.LOGGER.error(f"Exception when creating HousekeeperController instance: {e}")

    return nseploymentFactory

def __init__(self):
    pass

# @staticmethod should _load_from_parameterstore etc become static?
def make_nsdeployment(self, key_: str, ssm_client_, string_utils_: StringUtils, aws_service_: AWSService,
                      kubernetes_service_: KubernetesService, tick_engine_: TickEngine) -> Union[
    NSDeployment, None]:
    try:
        deployment = self._load_from_parameterstore(parameter_store_key_=key_, ssm_client_=ssm_client_,
                                                    string_utils_=string_utils_, aws_service_=aws_service_,
                                                    kubernetes_service_=kubernetes_service_)
        deployment.initialize(string_utils_=string_utils_, kubernetes_service_=kubernetes_service_,
                              tick_engine_=tick_engine_)

        deployments_to_apply = []  # .append
        self.collect_applies(deployment, deployments_to_apply, ssm_client_=ssm_client_, string_utils_=string_utils_,
                             aws_service_=aws_service_, kubernetes_service_=kubernetes_service_)
        for d in deployments_to_apply:
            if d is not deployment:
                deployment.apply_operations(d)
        return deployment
    except Exception as e:
        NSDeploymentFactory.LOGGER.error(f"Exception: {e}")
        return None

@staticmethod
def _load_from_parameterstore(parameter_store_key_: str, ssm_client_, string_utils_: StringUtils,
                              aws_service_: AWSService, kubernetes_service_: KubernetesService):
    try:
        housekeeper_deployment = NSDeploymentFactory._load_from_ps(parameter_store_key_, ssm_client_=ssm_client_,
                                                                   string_utils_=string_utils_,
                                                                   aws_service_=aws_service_,
                                                                   kubernetes_service_=kubernetes_service_)
        if housekeeper_deployment is not None:
            housekeeper_deployment_operation_data = NSDeploymentFactory._load_from_ps(
                parameter_store_key_ + "_data", ssm_client_=ssm_client_, string_utils_=string_utils_,
                aws_service_=aws_service_, kubernetes_service_=kubernetes_service_)
            if housekeeper_deployment_operation_data is not None:
                housekeeper_deployment.operation_data = housekeeper_deployment_operation_data.get_operation_data()
            return housekeeper_deployment
        else:
            return None
    except Exception as e:
        NSDeploymentFactory.LOGGER.error(f"Exception: {e}")
    return None

@staticmethod
def _load_from_ps(parameter_store_key, ssm_client_, string_utils_: StringUtils, aws_service_: AWSService,
                  kubernetes_service_: KubernetesService) -> Optional[NSDeployment]:
    """Load deployment configuration from Parameter Store using the injected SSM client."""
    try:
        response = ssm_client_.get_parameter(Name=parameter_store_key, WithDecryption=True)
        parameter_value = response["Parameter"]["Value"]
        ret = NSDeploymentFactory.load_from_string(parameter_value, ssm_client_=ssm_client_,
                                                   string_utils_=string_utils_, aws_service_=aws_service_,
                                                   kubernetes_service_=kubernetes_service_)
        if not ret:
            NSDeploymentFactory.LOGGER.info(f"Could not load or parse {parameter_store_key}")
            return None

        NSDeploymentFactory.LOGGER.info(f"{parameter_store_key} read.")
        ret.source = parameter_value
        ret.parameter_store_key = parameter_store_key
        return ret

    except ClientError as e:
        if e.response["Error"]["Code"] == "ParameterNotFound":
            NSDeploymentFactory.LOGGER.info(f"Parameter {parameter_store_key} not found.")
        else:
            NSDeploymentFactory.LOGGER.error(f"Error when retrieving {parameter_store_key}: {e}")
        return None
    except Exception as e:
        NSDeploymentFactory.LOGGER.error(f"General exception: {e}")
        return None

@staticmethod
def collect_applies(current_deployment_, deployments_to_apply_, ssm_client_, string_utils_: StringUtils,
                    aws_service_: AWSService, kubernetes_service_: KubernetesService):
    try:
        if not current_deployment_:
            return
        strings = current_deployment_.source.splitlines()
        deployments_to_apply_.append(current_deployment_)
        for s in strings:
            if s.startswith("#apply "):
                import_key = s[len("#apply "):]
                print(f"******** applying  {import_key}")
                d = NSDeploymentFactory._load_from_parameterstore(import_key, ssm_client_=ssm_client_,
                                                                  string_utils_=string_utils_,
                                                                  aws_service_=aws_service_,
                                                                  kubernetes_service_=kubernetes_service_)
                # deployments_to_apply.append(d)
                NSDeploymentFactory.collect_applies(d, deployments_to_apply_, ssm_client_=ssm_client_,
                                                    string_utils_=string_utils_, aws_service_=aws_service_,
                                                    kubernetes_service_=kubernetes_service_)
    except Exception as e:
        NSDeploymentFactory.LOGGER.error(f"Exception: {e}")

@staticmethod
def load_from_string(input_str_, ssm_client_, string_utils_: StringUtils, aws_service_: AWSService,
                     kubernetes_service_: KubernetesService) -> Optional[NSDeployment]:
    if string_utils_.is_yaml(input_str_) is True:
        depl = NSDeploymentFactory.load_from_yaml(input_str_, ssm_client_=ssm_client_, string_utils_=string_utils_,
                                                 aws_service_=aws_service_, kubernetes_service_=kubernetes_service_)
        if depl is not None:
            depl._is_yaml = True
        return depl
    else:
        NSDeploymentFactory.LOGGER.error("Input string is neither valid JSON nor YAML.")
        return None

@staticmethod
def _load_from_yaml(yaml_str_, ssm_client_, string_utils_: StringUtils, aws_service_: AWSService,
                    kubernetes_service_: KubernetesService):
    try:
        data = yaml.safe_load(yaml_str_)
        return NSDeploymentFactory._load(data, ssm_client_=ssm_client_, string_utils_=string_utils_,
                                         aws_service_=aws_service_, kubernetes_service_=kubernetes_service_)
    except Exception as e:
        NSDeploymentFactory.LOGGER.error(f"Exception: {e}")
        return None

@staticmethod
def load_from_yaml(yaml_str_, ssm_client_, string_utils_: StringUtils, aws_service_: AWSService,
                   kubernetes_service_: KubernetesService) -> Optional[NSDeployment]:
    """Loads a deployment from a YAML string."""
    try:
        data = yaml.safe_load(yaml_str_)
        return NSDeploymentFactory._load(data, ssm_client_=ssm_client_, string_utils_=string_utils_,
                                         aws_service_=aws_service_, kubernetes_service_=kubernetes_service_)
    except Exception as e:
        NSDeploymentFactory.LOGGER.error(f"Exception in load_from_yaml: {e}")
        return None

@staticmethod
def _load(data_, ssm_client_, string_utils_: StringUtils, aws_service_: AWSService,
          kubernetes_service_: KubernetesService) -> Optional[NSDeployment]:
    """Internal method to reconstruct HouseKeeperDeployment from data."""
    try:
        operations = []
        if data_.get("operations"):
            for item in data_["operations"]:
                if item["type"] == "ScaleOperation":
                    operation = ScaleOperation(item.get("id"), item.get("type"), item.get("execution_time"),
                                               item.get("target_percentage"), item.get("watermark_id"))
                elif item["type"] == "WatcherOperation":
                    operation = WatcherOperation(item.get("id"), item.get("type"), item.get("execution_time"),
                                                 item.get("last_target_scale_id"))
                elif item["type"] == "ImmediateScaleOperation":
                    operation = ImmediateScaleOperation(item.get("id"), item.get("type"), item.get("scale_to"))
                elif item["type"] == "WatermarkOperation":
                    operation = WatermarkOperation(item.get("id"), item.get("type"), item.get("execution_time"),
                                                   item.get("target_percentage"), item.get("watermark_id"))
                elif item["type"] == "TransactionBeginOperation":
                    operation = TransactionBeginOperation(item.get("id"), item.get("type"),
                                                          item.get("execution_time"), item.get("target_percentage"))
                elif item["type"] == "DefineTransactionIdPrefixOperation":
                    operation = DefineTransactionIdPrefixOperation(item.get("id"), item.get("type"),
                                                                   item.get("execution_time"),
                                                                   item.get("target_percentage"),
                                                                   item.get("transaction_id_prefix"))
                elif item["type"] == "TransactionEndOperation":
                    operation = TransactionEndOperation(item.get("id"), item.get("type"),
                                                        item.get("execution_time"), item.get("target_percentage"))
                else:
                    operation = Operation(item.get("id"), item.get("type"), item.get("execution_time"),
                                          item.get("target_percentage"))

                operation.initialize(ssm_client_=ssm_client_, string_utils_=string_utils_,
                                     aws_service_=aws_service_, kubernetes_service_=kubernetes_service_)
                operations.append(operation)
        operation_data = []
        if data_.get("operation_data") is not None:
            try:
                operation_data = [Datum.from_dict(datum) for datum in data_["operation_data"]]
            except Exception as e:
                operation_data = []

        return NSDeployment(name=data_.get("name"), type=data_.get("type"), id=data_.get("id"),
                            version=data_.get("version"), url=data_.get("url"), env=data_.get("env"),
                            sealId=data_.get("sealId"), operations=operations, operation_data=operation_data,
                            skip_kubernetes_scale=data_.get("skip_kubernetes_scale"),
                            skip_parameterstore_write=data_.get("skip_parameterstore_write"),
                            active=data_.get("active"))
    except Exception as e:
        NSDeploymentFactory.LOGGER.error(f"Exception in {inspect.currentframe().f_code.co_name}: {e}")
        return None