class Commands(metaclass=abc.ABCMeta):
    ''' All the different cloud provider controllers must inherit
    from this class to ensure that the commands are defined consistently'''

    @abc.abstractmethod
    def init(self):
        pass

    @abc.abstractmethod
    def invoke(self):
        pass

    @abc.abstractmethod
    def run(self):
        pass

    @abc.abstractmethod    
    def ls(self):
        pass

    @abc.abstractmethod
    def rm(self):
        pass

    @abc.abstractmethod
    def log(self):
        pass

    @abc.abstractmethod
    def put(self):
        pass

    @abc.abstractmethod
    def get(self):
        pass class APIGateway(GenericClient):
    """Manage the calls to the ApiGateway client."""

    def __init__(self, resources_info: Dict):
        super().__init__(resources_info.get('api_gateway', {}))
        self.resources_info = resources_info
        self.api = self.resources_info.get('api_gateway', {})

    def _get_common_args(self) -> Dict:
        return {'restApiId' : self.api.get('id', ''),
                'resourceId' : self.api.get('resource_id', ''),
                'httpMethod' : self.api.get('http_method', '')}

    def _get_method_args(self) -> Dict:
        args = self._get_common_args()
        args.update(self.api.get('method', {}))
        return args

    def _get_integration_args(self) -> Dict:
        integration_args = self.api.get('integration', {})
        uri_args = {'api_region': self.api.get('region', ''),
                    'lambda_region': self.resources_info.get('lambda', {}).get('region', ''),
                    'account_id': self.resources_info.get('iam', {}).get('account_id', ''),
                    'function_name': self.resources_info.get('lambda', {}).get('name', '')}
        integration_args['uri'] = integration_args['uri'].format(**uri_args)
        args = self._get_common_args()
        args.update(integration_args)
        return args

    def _get_resource_id(self) -> str:
        res_id = ""
        resources_info = self.client.get_resources(self.api.get('id', ''))
        for resource in resources_info['items']:
            if resource['path'] == '/':
                res_id = resource['id']
                break
        return res_id

    def _set_api_gateway_id(self, api_info: Dict) -> None:
        self.api['id'] = api_info.get('id', '')
        # We store the parameter in the lambda configuration that
        # is going to be uploaded to the Lambda service
        self.resources_info['lambda']['environment']['Variables']['API_GATEWAY_ID'] = api_info.get('id', '')

    def _set_resource_info_id(self, resource_info: Dict) -> None:
        self.api['resource_id'] = resource_info.get('id', '')

    def _get_endpoint(self) -> str:
        endpoint_args = {'api_id': self.api.get('id', ''),
                         'api_region': self.api.get('region', ''),
                         'stage_name': self.api.get('stage_name', '')}
        return self.api.get('endpoint', '').format(**endpoint_args)

    def create_api_gateway(self) -> None:
        """Creates an Api Gateway endpoint."""
        api_info = self.client.create_rest_api(self.api.get('name', ''))
        self._set_api_gateway_id(api_info)
        resource_info = self.client.create_resource(self.api.get('id', ''),
                                                    self._get_resource_id(),
                                                    self.api.get('path_part', ''))
        self._set_resource_info_id(resource_info)
        self.client.create_method(**self._get_method_args())
        self.client.set_integration(**self._get_integration_args())
        self.client.create_deployment(self.api.get('id', ''), self.api.get('stage_name', ''))
        logger.info(f'API Gateway endpoint: {self._get_endpoint()}')

    def delete_api_gateway(self) -> None:
        """Deletes an Api Gateway endpoint."""
        return self.client.delete_rest_api(self.resources_info['lambda']['environment']['Variables']['API_GATEWAY_ID']) class Batch(GenericClient):

    def __init__(self, resources_info):
        super().__init__(resources_info.get('batch'))
        self.resources_info = resources_info
        self.batch = resources_info.get('batch')
        self.function_name = self.resources_info.get('lambda').get('name')

    def _set_required_environment_variables(self) -> None:
        self._set_batch_environment_variable('AWS_LAMBDA_FUNCTION_NAME', self.function_name)
        self._set_batch_environment_variable('SCRIPT', self._get_user_script())
        self._set_batch_environment_variable('FUNCTION_CONFIG', self._get_config_file())
        if self.resources_info.get('lambda').get('container').get('environment').get('Variables', False):
            for key, value in self.resources_info.get('lambda').get('container').get('environment').get('Variables').items():
                self._set_batch_environment_variable(key, value)

    def _set_batch_environment_variable(self, key: str, value: str) -> None:
        self.resources_info['batch']['environment']['Variables'].update({key: value})

    def _get_user_script(self) -> str:
        script = ''
        if self.resources_info.get('lambda').get('init_script', False):
            file_content = FileUtils.read_file(self.resources_info.get('lambda').get('init_script'))
            script = StrUtils.utf8_to_base64_string(file_content)
        return script

    def _get_config_file(self) -> str:
        cfg_file = ''
        config = create_function_config(self.resources_info)
        yaml_str = yaml.safe_dump(config)
        cfg_file = StrUtils.utf8_to_base64_string(yaml_str)
        return cfg_file

    def _delete_job_definitions(self) -> None:
        # Get main job definition
        kwargs = {"jobDefinitionName": self.function_name}
        job_info = self.client.describe_job_definitions(**kwargs)
        for job_def in _get_job_definitions(job_info):
            kwars = {"jobDefinition": job_def}
            self.client.deregister_job_definition(**kwars)
        logger.info("Job definitions successfully deleted.")

    def _get_job_queue_info(self):
        job_queue_info_args = {'jobQueues': [self.function_name]}
        return self.client.describe_job_queues(**job_queue_info_args)

    def _delete_job_queue(self):
        response = self._get_job_queue_info()
        while response["jobQueues"]:
            state = response["jobQueues"][0]["state"]
            status = response["jobQueues"][0]["status"]
            if status == "VALID":
                self._delete_valid_job_queue(state)
            response = self._get_job_queue_info()

    def _delete_valid_job_queue(self, state):
        if state == "ENABLED":
            updating_args = {'jobQueue': self.function_name,
                             'state': 'DISABLED'}
            self.client.update_job_queue(**updating_args)
        elif state == "DISABLED":
            deleting_args = {'jobQueue': self.function_name}
            self.client.delete_job_queue(**deleting_args)
            logger.info("Job queue successfully deleted.")

    def _get_describe_compute_env_args(self):
        return {'computeEnvironments': [self.function_name]}

    def _get_compute_env_info(self):
        creation_args = self._get_describe_compute_env_args()
        return self.client.describe_compute_environments(**creation_args)

    def _delete_compute_env(self):
        response = self._get_compute_env_info()
        while response["computeEnvironments"]:
            state = response["computeEnvironments"][0]["state"]
            status = response["computeEnvironments"][0]["status"]
            if status == "VALID":
                self._delete_valid_compute_environment(state)
            response = self._get_compute_env_info()

    def _delete_valid_compute_environment(self, state):
        if state == "ENABLED":
            update_args = {'computeEnvironment': self.function_name,
                           'state': 'DISABLED'}
            self.client.update_compute_environment(**update_args)
        elif state == "DISABLED":
            delete_args = {'computeEnvironment': self.function_name}
            self.client.delete_compute_environment(**delete_args)
            logger.info("Compute environment successfully deleted.")

    def _get_compute_env_args(self):
        account_id = self.resources_info.get('iam').get('account_id')
        return {
            'computeEnvironmentName': self.function_name,
            'serviceRole': self.batch.get('service_role').format(account_id=account_id),
            'type': self.batch.get('type'),
            'state':  self.batch.get('state'),
            'computeResources': {
                'type': self.batch.get('compute_resources').get('type'),
                'minvCpus': self.batch.get('compute_resources').get('min_v_cpus'),
                'maxvCpus': self.batch.get('compute_resources').get('max_v_cpus'),
                'desiredvCpus': self.batch.get('compute_resources').get('desired_v_cpus'),
                'instanceTypes': self.batch.get('compute_resources').get('instance_types'),
                'subnets': self.batch.get('compute_resources').get('subnets'),
                'securityGroupIds': self.batch.get('compute_resources').get('security_group_ids'),
                'instanceRole': self.batch.get('compute_resources').get('instance_role').format(account_id=account_id),
                'launchTemplate': {
                    'launchTemplateName': self.batch.get('compute_resources').get('launch_template_name'),
                    'version': str(LaunchTemplates(self.resources_info).get_launch_template_version())
                }
            }
        }

    def _get_creations_job_queue_args(self):
        return {
            'computeEnvironmentOrder': [{'computeEnvironment': self.function_name,
                                         'order': 1}, ],
            'jobQueueName':  self.function_name,
            'priority': 1,
            'state': self.batch.get('state'),
        }

    def _get_job_definition_args(self):
        job_def_args = {
            'jobDefinitionName': self.function_name
        }
        if self.batch.get('multi_node_parallel').get('enabled'):
            job_def_args['nodeProperties'] = self._get_node_properties_multi_node_args()
            job_def_args['type'] = 'multinode'
        else:
            job_def_args['containerProperties'] = self._get_container_properties_single_node_args()
            job_def_args['type'] = 'container'
        return job_def_args

    def _get_container_properties_single_node_args(self):
        job_def_args = {
            'image': self.resources_info.get('lambda').get('container').get('image'),
            'memory': int(self.batch.get('memory')),
            'vcpus': int(self.batch.get('vcpus')),
            'command': [
                '/bin/sh',
                '-c',
                'echo $EVENT | /opt/faas-supervisor/bin/supervisor'
            ],
            'volumes': [
                {
                    'host': {
                        'sourcePath': '/opt/faas-supervisor/bin'
                    },
                    'name': 'supervisor-bin'
                }
            ],
            'environment': [{'name': key, 'value': value} for key, value in self.resources_info['batch']['environment']['Variables'].items()],
            'mountPoints': [
                {
                    'containerPath': '/opt/faas-supervisor/bin',
                    'sourceVolume': 'supervisor-bin'
                }
            ]
        }
        if self.batch.get('enable_gpu'):
            job_def_args['resourceRequirements'] = [
                {
                    'value': '1',
                    'type': 'GPU'
                }
            ]
        return job_def_args

    def _get_node_properties_multi_node_args(self):
        #targetNodes = self.batch.get('multi_node_parallel').get('number_nodes') - 1
        job_def_args = {
            "numNodes": int(self.batch.get('multi_node_parallel').get('number_nodes')),
            "mainNode": int(self.batch.get('multi_node_parallel').get('main_node_index')),
            "nodeRangeProperties": [
                {
                "targetNodes": "0:", #+ str(targetNodes),
                "container": self._get_container_properties_single_node_args()
                }
            ]#[self._get_node_node_range_property_multi_node_args(target_nodes) for target_nodes in self.batch.get('multi_node_parallel').get('target_nodes')]
        }
        return job_def_args

    def _get_state_and_status_of_compute_env(self):
        creation_args = self._get_describe_compute_env_args()
        response = self.client.describe_compute_environments(**creation_args)
        return (response["computeEnvironments"][0]["state"],
                response["computeEnvironments"][0]["status"])

    def create_batch_environment(self):
        self._set_required_environment_variables()
        creation_args = self._get_compute_env_args()
        self.client.create_compute_environment(**creation_args)
        while True:
            state, status = self._get_state_and_status_of_compute_env()
            if state == "ENABLED" and status == "VALID":
                creation_args = self._get_creations_job_queue_args()
                logger.info("Compute environment successfully created.")
                self.client.create_job_queue(**creation_args)
                logger.info('Job queue successfully created.')
                creation_args = self._get_job_definition_args()
                logger.info(f"Registering '{self.function_name}' job definition.")
                return self.client.register_job_definition(**creation_args)

    def delete_compute_environment(self):
        self._delete_job_definitions()
        self._delete_job_queue()
        self._delete_compute_env()

    def exist_compute_environments(self):
        creation_args = self._get_describe_compute_env_args()
        response = self.client.describe_compute_environments(**creation_args)
        return len(response["computeEnvironments"]) > 0

    def get_jobs_with_request_id(self) -> Dict:
        describe_args = {'jobs': [self.resources_info.get('cloudwatch').get('request_id')]}
        return self.client.describe_jobs(**describe_args) class CloudWatchLogs(GenericClient):
    """Manages the AWS CloudWatch Logs functionality"""

    def __init__(self, resources_info: Dict):
        super().__init__(resources_info.get('cloudwatch'))
        self.resources_info = resources_info
        self.cloudwatch = resources_info.get('cloudwatch')

    def get_log_group_name(self, function_name: str=None) -> str:
        """Returns the log group matching the
        current lambda function being parsed."""
        if function_name:
            return f'/aws/lambda/{function_name}'
        return f'/aws/lambda/{self.resources_info.get("lambda").get("name")}'

    def _get_log_group_name_arg(self, function_name: str=None) -> Dict:
        return {'logGroupName' : self.get_log_group_name(function_name)}

    def _is_end_line(self, line: str) -> bool:
        return line.startswith('REPORT') and self.cloudwatch.get('request_id') in line

    def _is_start_line(self, line: str) -> bool:
        return line.startswith('START') and self.cloudwatch.get('request_id') in line

    def _parse_logs_with_requestid(self, function_logs: str) -> str:
        parsed_msg = ""
        if function_logs:
            in_req_id_logs = False
            for line in function_logs.split('\n'):
                if self._is_start_line(line):
                    parsed_msg += f'{line}\n'
                    in_req_id_logs = True
                elif self._is_end_line(line):
                    parsed_msg += line
                    break
                elif in_req_id_logs:
                    parsed_msg += f'{line}\n'
        return parsed_msg

    def _get_lambda_logs(self):
        """Returns Lambda logs for an specific lambda function."""
        function_logs = ""
        try:
            kwargs = self._get_log_group_name_arg()
            if self.cloudwatch.get("log_stream_name", False):
                kwargs["logStreamNames"] = [self.cloudwatch.get("log_stream_name")]
            function_logs = _parse_events_in_message(self.client.get_log_events(**kwargs))
            if self.cloudwatch.get("request_id", False):
                function_logs = self._parse_logs_with_requestid(function_logs)
        except ClientError as cerr:
            logger.warning("Error getting the function logs: %s" % cerr)
        return function_logs

    def _get_batch_job_log(self, jobs_info: List) -> str:
        """Returns Batch logs for an specific job."""
        batch_logs = ""
        if jobs_info:
            job = jobs_info[0]
            batch_logs += f"Batch job status: {job.get('status', '')}\n"
            kwargs = {'logGroupName': "/aws/batch/job"}
            if job.get("status", "") == "SUCCEEDED":
                kwargs['logStreamNames'] = [job.get("container", {}).get("logStreamName", "")]
                batch_events = self.client.get_log_events(**kwargs)
                msgs = [event.get('message', '')
                        for event in batch_events]
                batch_logs += '\n'.join(msgs)
        return batch_logs

    def create_log_group(self) -> Dict:
        """Creates a CloudWatch Log Group."""
        creation_args = self._get_log_group_name_arg()
        creation_args['tags'] = self.resources_info.get('lambda').get('tags')
        response = self.client.create_log_group(**creation_args)
        # Set retention policy into the log group
        retention_args = self._get_log_group_name_arg()
        retention_args['retentionInDays'] = self.cloudwatch.get('log_retention_policy_in_days')
        self.client.set_log_retention_policy(**retention_args)
        return response

    def delete_log_group(self, log_group_name: str) -> Dict:
        """Deletes a CloudWatch Log Group."""
        return self.client.delete_log_group(log_group_name)

    def get_aws_logs(self) -> str:
        """Returns Cloudwatch logs for an specific lambda function and batch job (if any)."""
        aws_logs = self._get_lambda_logs()
        batch_logs = ""
        if self.resources_info.get('cloudwatch').get('request_id', False):
            batch_jobs = Batch(self.resources_info).get_jobs_with_request_id()
            batch_logs = self._get_batch_job_log(batch_jobs["jobs"])
        return aws_logs + batch_logs if batch_logs else aws_logs class IAM(GenericClient):

    def __init__(self, resources_info) -> None:
        super().__init__(resources_info.get('iam'))

    def get_user_name_or_id(self):
        user = self.client.get_user_info()
        if user:
            return user.get('UserName', user['User']['UserId']) class Lambda(GenericClient):

    def __init__(self, resources_info: Dict) -> None:
        super().__init__(resources_info.get('lambda', {}))
        self.resources_info = resources_info
        self.function = resources_info.get('lambda', {})
        self.supervisor_version = resources_info.get('lambda').get('supervisor').get('version')
        if (self.function.get('runtime') == "image" and
                StrUtils.compare_versions(self.supervisor_version, "1.5.0b3") < 0):
            # In case of using image runtime
            # it must be 1.5.0-beta3 version or higher
            raise Exception("Supervisor version must be 1.5.0 or higher for image runtime.")

    def _get_creations_args(self, zip_payload_path: str, supervisor_zip_path: str) -> Dict:
        args = {'FunctionName': self.function.get('name'),
                'Role': self.resources_info.get('iam').get('role'),
                'Environment': self.function.get('environment'),
                'Description': self.function.get('description'),
                'Timeout':  self.function.get('timeout'),
                'MemorySize': self.function.get('memory'),
                'Tags': self.function.get('tags'),
                'Architectures': self.function.get('architectures', ['x86_64'])}
        if self.function.get('vpc'):
            args['VpcConfig'] = self.function.get('vpc')
        if self.function.get('file_system'):
            args['FileSystemConfigs'] = self.function.get('file_system')
        if self.function.get('runtime') == "image":
            args['Code'] = {'ImageUri': self.function.get('container').get('image')}
            args['PackageType'] = 'Image'
        else:
            args['Code'] = self._get_function_code(zip_payload_path, supervisor_zip_path)
            args['Runtime'] = self.function.get('runtime')
            args['Handler'] = self.function.get('handler')
            args['Layers'] = self.function.get('layers')
        return args

    def is_asynchronous(self):
        return self.function.get('asynchronous', False)

    def get_access_key(self) -> str:
        """Returns the access key belonging to the boto_profile used."""
        return self.client.get_access_key()

    @excp.exception(logger)
    def create_function(self):
        # Create tmp folders
        zip_payload_path = None
        supervisor_zip_path = None
        if self.function.get('runtime') == "image":
            # Create docker image in ECR
            self.function['container']['image'] = ContainerImage.create_ecr_image(self.resources_info,
                                                                                  self.supervisor_version)
        else:
            # Check if supervisor's source is already cached
            cached, supervisor_zip_path = SupervisorUtils.is_supervisor_cached(self.supervisor_version)
            if not cached:
                # Download supervisor
                supervisor_zip_path = SupervisorUtils.download_supervisor(self.supervisor_version)
            # Manage supervisor layer
            self._manage_supervisor_layer(supervisor_zip_path)
            # Create function
            tmp_folder = FileUtils.create_tmp_dir()
            zip_payload_path = FileUtils.join_paths(tmp_folder.name, 'function.zip')
        self._set_image_id()
        self._set_fdl()
        creation_args = self._get_creations_args(zip_payload_path, supervisor_zip_path)
        response = self.client.create_function(**creation_args)
        if response and "FunctionArn" in response:
            self.function['arn'] = response.get('FunctionArn', "")
        return response

    def _set_image_id(self):
        image = self.function.get('container').get('image')
        if image:
            self.function['environment']['Variables']['IMAGE_ID'] = image

    def _set_fdl(self):
        fdl = StrUtils.dict_to_base64_string(create_function_config(self.resources_info))
        self.function['environment']['Variables']['FDL'] = fdl

    def _manage_supervisor_layer(self, supervisor_zip_path: str) -> None:
        layers_client = LambdaLayers(self.resources_info, self.client, supervisor_zip_path)
        self.function.get('layers', []).append(layers_client.get_supervisor_layer_arn())

    @excp.exception(logger)
    def _get_function_code(self, zip_payload_path: str, supervisor_zip_path: str) -> Dict:
        '''Zip all the files and folders needed.'''
        code = {}
        FunctionPackager(self.resources_info, supervisor_zip_path).create_zip(zip_payload_path)
        if self.function.get('deployment').get('bucket', False):
            file_key = f"lambda/{self.function.get('name')}.zip"
            s3_client = S3(self.resources_info)
            s3_client.create_bucket(self.function.get('deployment').get('bucket'))
            s3_client.upload_file(bucket=self.function.get('deployment').get('bucket'),
                                  file_path=zip_payload_path,
                                  file_key=file_key)
            code = {"S3Bucket": self.function.get('deployment').get('bucket'),
                    "S3Key": file_key}
        else:
            code = {"ZipFile": FileUtils.read_file(zip_payload_path, mode="rb")}
        return code

    def delete_function(self):
        function_name = self.resources_info.get('lambda').get('name')
        fdl = self.get_fdl_config(function_name)
        res = self.client.delete_function(function_name)
        runtime = fdl.get('runtime', self.function.get('runtime'))
        if runtime == "image":
            ecr_info = self.resources_info.get('ecr', {'delete_image': True})
            ecr_info.update(fdl.get('ecr', {}))
            # only delete the image if delete_image is True and create_image was True
            if ecr_info.get('delete_image') and fdl.get('container', {}).get('create_image', True):
                ContainerImage.delete_ecr_image(self.resources_info)
        return res

    def link_function_and_bucket(self, bucket_name: str) -> None:
        kwargs = {'FunctionName': self.function.get('name'),
                  'Principal': "s3.amazonaws.com",
                  'SourceArn': f'arn:aws:s3:::{bucket_name}'}
        self.client.add_invocation_permission(**kwargs)

    def preheat_function(self):
        logger.info("Preheating function")
        self._set_request_response_call_parameters()
        self.launch_lambda_instance()
        logger.info("Preheating successful")

    def _launch_async_event(self, s3_event):
        self.set_asynchronous_call_parameters()
        return self._launch_s3_event(s3_event)

    def launch_request_response_event(self, s3_event):
        self._set_request_response_call_parameters()
        return self._launch_s3_event(s3_event)

    def _launch_s3_event(self, s3_event):
        self.function['payload'] = s3_event
        logger.info(f"Sending event for file '{s3_event['Records'][0]['s3']['object']['key']}'")
        return self.launch_lambda_instance()

    def process_asynchronous_lambda_invocations(self, s3_event_list):
        if (len(s3_event_list) > MAX_CONCURRENT_INVOCATIONS):
            for s3_file_chunk in DataTypesUtils.divide_list_in_chunks(s3_event_list, MAX_CONCURRENT_INVOCATIONS):
                self._launch_concurrent_lambda_invocations(s3_file_chunk)
        else:
            self._launch_concurrent_lambda_invocations(s3_event_list)

    def _launch_concurrent_lambda_invocations(self, s3_event_list):
        pool = ThreadPool(processes=len(s3_event_list))
        pool.map(lambda s3_event: self._launch_async_event(s3_event), s3_event_list)
        pool.close()

    def launch_lambda_instance(self):
        if self.is_asynchronous():
            self.set_asynchronous_call_parameters()
        response = self._invoke_lambda_function()
        response_args = {'Response': response,
                         'FunctionName': self.function.get('name'),
                         'IsAsynchronous': self.function.get('asynchronous')}
        return response_args

    def _get_invocation_payload(self):
        # Default payload
        payload = self.function.get('payload', {})
        if not payload:
            # Check for defined run script
            if self.function.get("run_script", False):
                script_path = self.function.get("run_script")
                # We first code to base64 in bytes and then decode those bytes to allow the json lib to parse the data
                # https://stackoverflow.com/questions/37225035/serialize-in-json-a-base64-encoded-data#37239382
                payload = {"script": StrUtils.bytes_to_base64str(FileUtils.read_file(script_path, 'rb'))}
            # Check for defined commands
            # This overrides any other function payload
            if self.function.get("c_args", False):
                payload = {"cmd_args": json.dumps(self.function.get("c_args"))}
        return json.dumps(payload)

    def _invoke_lambda_function(self):
        invoke_args = {'FunctionName':  self.function.get('name'),
                       'InvocationType':  self.function.get('invocation_type'),
                       'LogType':  self.function.get('log_type'),
                       'Payload': self._get_invocation_payload()}
        return self.client.invoke_function(**invoke_args)

    def set_asynchronous_call_parameters(self):
        self.function.update(ASYNCHRONOUS_CALL)

    def _set_request_response_call_parameters(self):
        self.function.update(REQUEST_RESPONSE_CALL)

    def _get_function_environment_variables(self):
        return self.get_function_configuration()['Environment']

    def merge_aws_and_local_configuration(self, aws_conf: Dict) -> Dict:
        result = ConfigFileParser().get_properties().get('aws')
        result['lambda']['name'] = aws_conf['FunctionName']
        result['lambda']['arn'] = aws_conf['FunctionArn']
        result['lambda']['timeout'] = aws_conf['Timeout']
        result['lambda']['memory'] = aws_conf['MemorySize']
        if 'Environment' in result:
            result['lambda']['environment']['Variables'] = aws_conf['Environment']['Variables'].copy()
        if 'Layers' in result:
            result['lambda']['layers'] = aws_conf['Layers'].copy()
        result['lambda']['supervisor']['version'] = aws_conf['SupervisorVersion']
        return result

    def get_all_functions(self, arn_list):
        try:
            return [self.merge_aws_and_local_configuration(self.get_function_configuration(function_arn))
                    for function_arn in arn_list]
        except ClientError as cerr:
            print(f"Error getting function info by arn: {cerr}")

    def get_function_configuration(self, arn: str = None) -> Dict:
        function = arn if arn else self.function.get('name')
        return self.client.get_function_configuration(function)

    def get_fdl_config(self, arn: str = None) -> Dict:
        function = arn if arn else self.function.get('name')
        function_info = self.client.get_function(function)
        # Get the FDL from the env variable
        fdl = function_info.get('Configuration', {}).get('Environment', {}).get('Variables', {}).get('FDL')
        if fdl:
            return yaml.safe_load(StrUtils.decode_base64(fdl))

        # In the future this part can be removed
        if 'Location' in function_info.get('Code'):
            dep_pack_url = function_info.get('Code').get('Location')
        else:
            return {}
        dep_pack = get_file(dep_pack_url)
        # Extract function_config.yaml
        try:
            with ZipFile(io.BytesIO(dep_pack)) as thezip:
                with thezip.open('function_config.yaml') as cfg_yaml:
                    return yaml.safe_load(cfg_yaml)
        except (KeyError, BadZipfile):
            return {}

    @excp.exception(logger)
    def find_function(self, function_name_or_arn=None):
        try:
            # If this call works the function exists
            name_arn = function_name_or_arn if function_name_or_arn else self.function.get('name', '')
            self.get_function_configuration(name_arn)
            return True
        except ClientError as ce:
            # Function not found
            if ce.response['Error']['Code'] == 'ResourceNotFoundException':
                return False
            else:
                raise

    def add_invocation_permission_from_api_gateway(self):
        api = self.resources_info.get('api_gateway')
        # Add Testing permission
        kwargs = {'FunctionName': self.function.get('name'),
                  'Principal': api.get('service_id'),
                  'SourceArn': api.get('source_arn_testing').format(api_region=api.get('region'),
                                                                    account_id=self.resources_info.get('iam').get('account_id'),
                                                                    api_id=api.get('id'))}
        self.client.add_invocation_permission(**kwargs)
        # Add Invocation permission
        kwargs['SourceArn'] = api.get('source_arn_invocation').format(api_region=api.get('region'),
                                                                      account_id=self.resources_info.get('iam').get('account_id'),
                                                                      api_id=api.get('id'),
                                                                      stage_name=api.get('stage_name'))
        self.client.add_invocation_permission(**kwargs)

    def get_api_gateway_id(self):
        env_vars = self._get_function_environment_variables()
        return env_vars['Variables'].get('API_GATEWAY_ID', '')

    def _get_api_gateway_url(self):
        api_id = self.get_api_gateway_id()
        if not api_id:
            raise excp.ApiEndpointNotFoundError(self.function.get('name'))
        return self.resources_info.get('api_gateway').get('endpoint').format(api_id=api_id,
                                                                             api_region=self.resources_info.get('api_gateway').get('region'),
                                                                             stage_name=self.resources_info.get('api_gateway').get('stage_name'))

    def call_http_endpoint(self):
        invoke_args = {'headers': {'X-Amz-Invocation-Type': 'Event'} if self.is_asynchronous() else {}}
        self._set_invoke_args(invoke_args)
        return call_http_endpoint(self._get_api_gateway_url(), **invoke_args)

    def _set_invoke_args(self, invoke_args):
        if self.resources_info.get('api_gateway').get('data_binary', False):
            invoke_args['data'] = self._get_b64encoded_binary_data()
            invoke_args['headers'].update({'Content-Type': 'application/octet-stream'})
        if self.resources_info.get('api_gateway').get('parameters', False):
            invoke_args['params'] = self._parse_http_parameters(self.resources_info.get('api_gateway').get('parameters'))
        if self.resources_info.get('api_gateway').get('json_data', False):
            invoke_args['data'] = self._parse_http_parameters(self.resources_info.get('api_gateway').get('json_data'))
            invoke_args['headers'].update({'Content-Type': 'application/json'})

    def _parse_http_parameters(self, parameters):
        return parameters if isinstance(parameters, dict) else json.loads(parameters)

    @excp.exception(logger)
    def _get_b64encoded_binary_data(self):
        data_path = self.resources_info.get('api_gateway').get('data_binary')
        AWSValidator.validate_http_payload_size(data_path, self.is_asynchronous())
        with open(data_path, 'rb') as data_file:
            return base64.b64encode(data_file.read())

    def wait_function_active(self, function_arn, max_time=60, delay=2):
        func = {"State": "Pending"}
        wait = 0
        while "State" in func and func["State"] == "Pending" and wait < max_time:
            func = self.get_function_configuration(function_arn)
            time.sleep(delay)
            wait += delay
        return func["State"] == "Active" class ResourceGroups(GenericClient):
    """Class to manage AWS Resource Groups"""

    def __init__(self, resources_info) -> None:
        super().__init__(resources_info.get('lambda'))

    def get_resource_arn_list(self, iam_user_id: str, resource_type: str = 'lambda') -> List:
        """Returns a list of ARNs filtered by the resource_type
        passed and the tags created by scar."""
        try:
            # Creation of a function_info filter by tags
            tag_filters = [{'Key': 'owner', 'Values': [iam_user_id]},
                           {'Key': 'createdby', 'Values': ['scar']}]
            resource_type_filters = [resource_type]
            tagged_resources = self.client.get_tagged_resources(tag_filters, resource_type_filters)
            return [function_info['ResourceARN'] for function_info in tagged_resources]
        except ClientError as cerr:
            logger.error("Error getting function_info arn by tag",
                         f"Error getting function_info arn by tag: {cerr}")
            raise cerr class S3(GenericClient):

    def __init__(self, resources_info):
        super().__init__(resources_info.get('s3'))
        self.resources_info = resources_info

    @excp.exception(logger)
    def create_bucket(self, bucket_name) -> None:
        if not self.client.find_bucket(bucket_name):
            self.client.create_bucket(bucket_name)

    @excp.exception(logger)
    def add_bucket_folder(self, bucket: str, folders: str) -> None:
        if not self.client.is_folder(bucket, folders):
            self.upload_file(bucket, folder_name=folders)

    def create_bucket_and_folders(self, storage_path: str) -> Tuple:
        bucket, folders = get_bucket_and_folders(storage_path)
        self.create_bucket(bucket)
        if folders:
            self.add_bucket_folder(bucket, folders)
        return bucket, folders

    def set_input_bucket_notification(self, bucket_name: str, folders: str) -> None:
        # First check that the function doesn't have other configurations
        bucket_conf = self.client.get_notification_configuration(bucket_name)
        trigger_conf = self.get_trigger_configuration(folders)
        lambda_conf = [trigger_conf]
        if "LambdaFunctionConfigurations" in bucket_conf:
            lambda_conf = bucket_conf["LambdaFunctionConfigurations"]
            lambda_conf.append(trigger_conf)
        notification = {"LambdaFunctionConfigurations": lambda_conf}
        self.client.put_notification_configuration(bucket_name, notification)

    def delete_bucket_notification(self, bucket_name):
        bucket_conf = self.client.get_notification_configuration(bucket_name)
        if bucket_conf and "LambdaFunctionConfigurations" in bucket_conf:
            lambda_conf = bucket_conf["LambdaFunctionConfigurations"]
            filter_conf = [x for x in lambda_conf if x['LambdaFunctionArn'] != self.resources_info.get('lambda').get('arn')]
            notification = {"LambdaFunctionConfigurations": filter_conf}
            self.client.put_notification_configuration(bucket_name, notification)
            logger.info("Bucket notifications successfully deleted.")

    def get_trigger_configuration(self, folders: str) -> Dict:
        conf = {"LambdaFunctionArn": self.resources_info.get('lambda').get('arn'),
                "Events": ["s3:ObjectCreated:*"]}
        if folders != '':
            conf['Filter'] = {"Key": {"FilterRules": [{"Name": "prefix", "Value": f'{folders}/'}]}}
        return conf

    def get_file_key(self, folder_name=None, file_path=None, file_key=None):
        if file_key:
            return file_key
        file_key = ''
        if file_path:
            file_key = os.path.basename(file_path)
            if folder_name:
                file_key = FileUtils.join_paths(folder_name, file_key)
        elif folder_name:
            file_key = folder_name if folder_name.endswith('/') else '{0}/'.format(folder_name)
        return file_key

    @excp.exception(logger)
    def upload_file(self, bucket: str, folder_name: str=None, file_path: str=None, file_key: str=None) -> None:
        kwargs = {'Bucket': bucket}
        kwargs['Key'] = self.get_file_key(folder_name, file_path, file_key)
        if file_path:
            try:
                kwargs['Body'] = FileUtils.read_file(file_path, 'rb')
            except FileNotFoundError:
                raise excp.UploadFileNotFoundError(file_path=file_path)
        if folder_name and not file_path:
            kwargs['ContentType'] = 'application/x-directory'
            logger.info(f"Folder '{kwargs['Key']}' created in bucket '{kwargs['Bucket']}'.")
        else:
            logger.info(f"Uploading file '{file_path}' to bucket '{kwargs['Bucket']}' with key '{kwargs['Key']}'.")
        self.client.upload_file(**kwargs)

    @excp.exception(logger)
    def get_bucket_file_list(self, storage: Dict=None):
        files = []
        if storage:
            files = self._list_storage_files(storage)
        else:
            for storage_info in self.resources_info.get('lambda').get('input'):
                if storage_info.get('storage_provider') == 's3':
                    files.extend(self._list_storage_files(storage_info))
        return files

    def _list_storage_files(self, storage: Dict) -> List:
        files = []
        bucket_name, folder_path = get_bucket_and_folders(storage.get('path'))
        if self.client.find_bucket(bucket_name):
            kwargs = {"Bucket" : bucket_name}
            if folder_path:
                kwargs["Prefix"] = folder_path
            files = (self.client.list_files(**kwargs))
        else:
            raise excp.BucketNotFoundError(bucket_name=bucket_name)
        return files

    def get_s3_event(self, bucket_name, file_key):
        event = self.resources_info.get("s3").get("event")
        event['Records'][0]['s3']['bucket']['name'] = bucket_name
        event['Records'][0]['s3']['bucket']['arn'] = event['Records'][0]['s3']['bucket']['arn'].format(bucket_name=bucket_name)
        event['Records'][0]['s3']['object']['key'] = file_key
        return event

    def get_s3_event_list(self, bucket_name, file_keys):
        return [self.get_s3_event(bucket_name, file_key) for file_key in file_keys]

    def download_file(self, bucket_name, file_key, file_path):
        kwargs = {'Bucket' : bucket_name, 'Key' : file_key}
        logger.info(f"Downloading file '{file_key}' from bucket '{bucket_name}' in path '{file_path}'.")
        with open(file_path, 'wb') as file:
            kwargs['Fileobj'] = file
            self.client.download_file(**kwargs) def get_bucket_and_folders(storage_path: str) -> Tuple:
    output_bucket = storage_path
    output_folders = ""
    output_path = storage_path.split("/", 1)
    if len(output_path) > 1:
        # There are folders defined
        output_bucket = output_path[0]
        output_folders = output_path[1]
    return (output_bucket, output_folders) class AWSValidator():
    """Class with methods to validate AWS properties."""

    @staticmethod
    def validate_kwargs(cls, **kwargs):
        aws_functions = kwargs.get('functions', {}).get('aws', {})
        for function in aws_functions:
            if 'iam' in function:
                cls.validate_iam(function['iam'])
            if 'lambda' in function:
                cls.validate_lambda(function['lambda'])
            if 'batch' in function:
                cls.validate_batch(function['batch'])

    @staticmethod
    def validate_iam(iam_properties):
        if ("role" not in iam_properties) or (iam_properties["role"] == ""):
            error_msg = ("Please, specify a valid iam role in the "
                         "configuration file (usually located in ~/.scar/scar.cfg).")
            raise ValidatorError(parameter='iam_role',
                                 parameter_value=iam_properties,
                                 error_msg=error_msg)

    @staticmethod
    def validate_lambda(cls, lambda_properties):
        if 'name' in lambda_properties:
            cls.validate_function_name(lambda_properties['name'])
        if 'memory' in lambda_properties:
            cls.validate_memory(lambda_properties['memory'])
        if 'time' in lambda_properties:
            cls.validate_time(lambda_properties['time'])

    @staticmethod
    def validate_batch(cls, batch_properties):
        if 'vcpus' in batch_properties:
            cls.validate_batch_vcpus(batch_properties['vcpus'])
        if 'memory' in batch_properties:
            cls.validate_batch_memory(batch_properties['memory'])
        if 'compute_resources' in batch_properties and \
            'comp_type' in batch_properties['compute_resources']:
            cls.validate_batch_comp_type(batch_properties['compute_resources']['comp_type'])

    @staticmethod
    def validate_time(lambda_time):
        if (lambda_time <= 0) or (lambda_time > 900):
            error_msg = 'Please, set a value between 0 and 900.'
            raise ValidatorError(parameter='lambda_time',
                                 parameter_value=lambda_time,
                                 error_msg=error_msg)

    @staticmethod
    def validate_memory(lambda_memory):
        if (lambda_memory < 128) or (lambda_memory > 3008):
            error_msg = 'Please, set a value between 128 and 3008.'
            raise ValidatorError(parameter='lambda_memory',
                                 parameter_value=lambda_memory,
                                 error_msg=error_msg)

    @staticmethod
    def validate_function_name(function_name):
        if not StrUtils.find_expression(function_name, VALID_LAMBDA_NAME_REGEX):
            error_msg = ("Find name restrictions in: https://docs.aws.amazon.com/lambda/latest/"
                         "dg/API_CreateFunction.html#SSS-CreateFunction-request-FunctionName")
            raise ValidatorError(parameter='function_name',
                                 parameter_value=function_name,
                                 error_msg=error_msg)

    @staticmethod
    def validate_function_code_size(scar_folder, max_payload_size):
        if FileUtils.get_tree_size(scar_folder) > max_payload_size:
            raise FunctionCodeSizeError(code_size='50MB')

    @staticmethod
    def validate_s3_code_size(scar_folder, max_s3_payload_size):
        if FileUtils.get_tree_size(scar_folder) > max_s3_payload_size:
            raise S3CodeSizeError(code_size='250MB')

    @staticmethod
    def validate_http_payload_size(file_path, async_call=False):
        file_size = FileUtils.get_file_size(file_path)
        if file_size > MAX_POST_BODY_SIZE:
            filesize = '{0:.2f}MB'.format(file_size / MB)
            maxsize = '{0:.2f}MB'.format(MAX_POST_BODY_SIZE / MB)
            raise InvocationPayloadError(file_size=filesize, max_size=maxsize)
        if async_call and file_size > MAX_POST_BODY_SIZE_ASYNC:
            filesize = '{0:.2f}KB'.format(file_size / KB)
            maxsize = '{0:.2f}KB'.format(MAX_POST_BODY_SIZE_ASYNC / KB)
            raise InvocationPayloadError(file_size=filesize, max_size=maxsize)

    @staticmethod
    def validate_batch_vcpus(batch_vcpus):
        if batch_vcpus < 1:
            error_msg = 'Please, set at least one vCPU.'
            raise ValidatorError(parameter='batch_vcpus',
                                 parameter_value=batch_vcpus,
                                 error_msg=error_msg)

    @staticmethod
    def validate_batch_memory(batch_memory):
        if batch_memory < 4:
            error_msg = 'Please, set a value greater than 4.'
            raise ValidatorError(parameter='batch_memory',
                                 parameter_value=batch_memory,
                                 error_msg=error_msg)

    @staticmethod
    def validate_batch_comp_type(batch_comp_type):
        if batch_comp_type not in ['SPOT', 'EC2']:
            error_msg = 'Please, set a valid compute environment type ("EC2" or "SPOT")'
            raise ValidatorError(parameter='batch_comp_type',
                                 parameter_value=batch_comp_type,
                                 error_msg=error_msg) class StrUtils:

    """Common methods for string management."""

    @staticmethod
    def decode_base64(value: Union[bytes, str]) -> bytes:
        """Decode a Base64 encoded bytes-like object or
        ASCII string and return the decoded bytes"""
        return base64.b64decode(value)

    @staticmethod
    def encode_base64(value: bytes) -> bytes:
        """Encode a bytes-like object using Base64
        and return the encoded bytes."""
        return base64.b64encode(value)

    @staticmethod
    def base64_to_utf8_string(value: Union[bytes, str]) -> str:
        """Decode a Base64 encoded bytes-like object or ASCII
        string and return the decoded value as a string."""
        return StrUtils.decode_base64(value).decode('utf-8')

    @staticmethod
    def utf8_to_base64_string(value: str) -> str:
        """Encode a 'utf-8' string using Base64 and return
        the encoded value as a string."""
        return StrUtils.encode_base64(bytes(value, 'utf-8')).decode('utf-8')

    @staticmethod
    def bytes_to_base64str(value, encoding='utf-8') -> str:
        """Encode a 'utf-8' string using Base64 and return
        the encoded value as a string."""
        return StrUtils.encode_base64(value).decode(encoding)

    @staticmethod
    def dict_to_base64_string(value: Dict) -> str:
        """Encodes a dictionary to base64 and returns a string."""
        return StrUtils.utf8_to_base64_string(json.dumps(value))

    @staticmethod
    def find_expression(string_to_search: str, rgx_pattern: str) -> Optional[str]:
        """Returns the first group that matches the rgx_pattern in the string_to_search."""
        if string_to_search:
            pattern = re.compile(rgx_pattern)
            match = pattern.search(string_to_search)
            if match:
                return match.group()
        return None

    @staticmethod
    def get_random_uuid4_str() -> str:
        """Returns a random generated uuid4 string."""
        return str(uuid.uuid4())

    @staticmethod
    def compare_versions(ver1: str, ver2: str) -> int:
        """Returns value < 0 to indicate that ver1 is less than ver2.
        Returns value > 0 to indicate that ver1 is greater than ver2.
        Returns value == 0 to indicate that ver1 is equal to ver2."""
        res = 0
        if version.parse(ver1) < version.parse(ver2):
            res = -1
        elif version.parse(ver1) > version.parse(ver2):
            res = 1
        return res class FileUtils:

    """Common methods for file and directory management."""

    @staticmethod
    def copy_file(source: str, dest: str) -> None:
        """Copy file to specified destination."""
        shutil.copy(source, dest)

    @staticmethod
    def copy_dir(source: str, dest: str) -> None:
        """Copy directory to specified destination."""
        dir_util.copy_tree(source, dest)

    @staticmethod
    def create_folder(folder_name):
        """Creates a system folder. Does nothing if the folder exists."""
        os.makedirs(folder_name, exist_ok=True)

    @staticmethod
    def get_scar_root_path() -> str:
        """Returns the root path of the project."""
        return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

    @staticmethod
    def join_paths(*paths: str) -> str:
        """Returns the strings passed joined as a system path."""
        return os.path.join(*paths)

    @staticmethod
    def get_tmp_dir() -> str:
        """Gets the directory where the temporal
        folder of the system is located."""
        return tempfile.gettempdir()

    @staticmethod
    def create_tmp_dir() -> tempfile.TemporaryDirectory:
        """Creates a directory in the temporal folder of the system.
        When the context is finished, the folder is automatically deleted."""
        return tempfile.TemporaryDirectory()

    @staticmethod
    def create_tmp_file(**kwargs) -> tempfile.NamedTemporaryFile:
        """Creates a directory in the temporal folder of the system.
        When the context is finished, the folder is automatically deleted."""
        return tempfile.NamedTemporaryFile(**kwargs)

    @staticmethod
    def get_tree_size(path: str) -> int:
        """Return total size of files in given path and subdirs."""
        total = 0
        for entry in os.scandir(path):
            if entry.is_dir(follow_symlinks=False):
                total += FileUtils.get_tree_size(entry.path)
            else:
                total += entry.stat(follow_symlinks=False).st_size
        return total

    @staticmethod
    def get_all_files_in_directory(dir_path: str) -> List[str]:
        """Returns a list with all the file paths in
        the specified directory and subdirectories."""
        files = []
        for dirname, _, filenames in os.walk(dir_path):
            for filename in filenames:
                files.append(os.path.join(dirname, filename))
        return files

    @staticmethod
    def get_file_size(file_path: str) -> int:
        """Returns the file size in bytes"""
        return os.stat(file_path).st_size

    @staticmethod
    def create_file_with_content(path: str,
                                 content: Optional[Union[str, bytes]],
                                 mode: str='w') -> None:
        """Creates a new file with the passed content.
        If the content is a dictionary, first is converted to a string."""
        with open(path, mode) as fwc:
            if isinstance(content, dict):
                content = json.dumps(content)
            fwc.write(content)

    @staticmethod
    def read_file(file_path: str, mode: str='r') -> Optional[Union[str, bytes]]:
        """Reads the whole specified file and returns the content."""
        with open(file_path, mode) as content_file:
            return content_file.read()

    @staticmethod
    def delete_file(path: str) -> None:
        """Delete the specified file."""
        if os.path.isfile(path):
            os.remove(path)

    @staticmethod
    def delete_folder(path: str) -> None:
        """Delete a folder with all its contents."""
        shutil.rmtree(path)

    @staticmethod
    def create_tar_gz(files_to_archive: List[str], destination_tar_path: str) -> str:
        """Create a .tar.gz file with the passed list of files."""
        with tarfile.open(destination_tar_path, 'w:gz') as tar:
            for file_path in files_to_archive:
                tar.add(file_path, arcname=os.path.basename(file_path))
        return destination_tar_path

    @staticmethod
    def extract_tar_gz(tar_path: str, destination_path: str) -> None:
        """Extract the content of a .tar.gz file in the specified path."""
        with tarfile.open(tar_path, 'r:gz') as tar:
            tar.extractall(path=destination_path)

    @staticmethod
    def unzip_folder(zip_path: str, folder_where_unzip_path: str, msg: str='') -> None:
        """Must use the unzip binary to preserve the file properties and the symlinks."""
        zip_exe = '/usr/bin/unzip'
        SysUtils.execute_command_with_msg([zip_exe, zip_path],
                                          cmd_wd=folder_where_unzip_path,
                                          cli_msg=msg)

    @staticmethod
    def zip_folder(zip_path: str, folder_to_zip_path: str, msg: str='') -> None:
        """Must use the zip binary to preserve the file properties and the symlinks."""
        zip_exe = '/usr/bin/zip'
        SysUtils.execute_command_with_msg([zip_exe, '-r9y', zip_path, '.'],
                                          cmd_wd=folder_to_zip_path,
                                          cli_msg=msg)

    @staticmethod
    def is_file(file_path: str):
        """Test whether a path is a regular file."""
        return os.path.isfile(file_path)

    @staticmethod
    def load_yaml(file_path: str) -> Dict:
        """Returns the content of a YAML file as a Dict."""
        if os.path.isfile(file_path):
            with open(file_path) as cfg_file:
                return yaml.safe_load(cfg_file)
        else:
            raise YamlFileNotFoundError(file_path=file_path)

    @staticmethod
    def write_yaml(file_path: str, content: Dict) -> None:
        with open(file_path, 'w') as cfg_file:
            yaml.safe_dump(content, cfg_file)

    @staticmethod
    def create_tmp_config_file(cfg_args, ConfigFileParser):
        cfg_path = ConfigFileParser.tmp_yaml_file_path
        os.environ['SCAR_TMP_CFG'] = cfg_path
        FileUtils.write_yaml(cfg_path, cfg_args)

    @staticmethod
    def load_tmp_config_file():
        return FileUtils.load_yaml(os.environ['SCAR_TMP_CFG'])

    @staticmethod
    def get_file_name(file_path: str) -> str:
        return os.path.basename(file_path)

    @staticmethod
    def extract_zip_from_url(url: str, dest_path: str) -> None:
        with ZipFile(BytesIO(url)) as thezip:
            thezip.extractall(dest_path) class SupervisorUtils:

    """Common methods for FaaS Supervisor management.
    https://github.com/grycap/faas-supervisor/"""

    _SUPERVISOR_GITHUB_REPO = 'faas-supervisor'
    _SUPERVISOR_GITHUB_USER = 'grycap'
    _SUPERVISOR_GITHUB_ASSET_NAME = 'supervisor'
    _SUPERVISOR_CACHE_DIR = '/var/tmp/cache/scar'
    _SUPERVISOR_SOURCE_NAME = 'faas-supervisor.zip'

    @classmethod
    def download_supervisor(cls, supervisor_version: str) -> str:
        """Downloads the FaaS Supervisor .zip package to the specified path."""
        path = FileUtils.join_paths(cls._SUPERVISOR_CACHE_DIR, supervisor_version)
        supervisor_zip_path = FileUtils.join_paths(path, cls._SUPERVISOR_SOURCE_NAME)
        supervisor_zip_url = GitHubUtils.get_source_code_url(
            cls._SUPERVISOR_GITHUB_USER,
            cls._SUPERVISOR_GITHUB_REPO,
            supervisor_version)
        with open(supervisor_zip_path, "wb") as thezip:
            thezip.write(request.get_file(supervisor_zip_url))
        return supervisor_zip_path

    @classmethod
    def check_supervisor_version(cls, supervisor_version: str) -> str:
        """Checks if the specified version exists in FaaS Supervisor's GitHub
        repository. Returns the version if exists and 'latest' if not."""
        if GitHubUtils.exists_release_in_repo(cls._SUPERVISOR_GITHUB_USER,
                                              cls._SUPERVISOR_GITHUB_REPO,
                                              supervisor_version):
            logger.info(f'Using supervisor release: \'{supervisor_version}\'.')
            return supervisor_version
        latest_version = SupervisorUtils.get_latest_release()
        if supervisor_version != 'latest':
            logger.info('Defined supervisor version does not exists.')
        logger.info(f'Using latest supervisor release: \'{latest_version}\'.')
        return latest_version

    @classmethod
    def get_supervisor_binary_url(cls, supervisor_version: str) -> str:
        """Returns the supervisor's binary download url."""
        return GitHubUtils.get_asset_url(cls._SUPERVISOR_GITHUB_USER,
                                         cls._SUPERVISOR_GITHUB_REPO,
                                         cls._SUPERVISOR_GITHUB_ASSET_NAME,
                                         supervisor_version)

    @classmethod
    def get_latest_release(cls) -> str:
        """Returns the latest FaaS Supervisor version."""
        return GitHubUtils.get_latest_release(cls._SUPERVISOR_GITHUB_USER,
                                              cls._SUPERVISOR_GITHUB_REPO)

    @classmethod
    def download_supervisor_asset(cls, version: str, asset_name: str, supervisor_zip_path: str) -> str:
        """Downloads the FaaS Supervisor asset to the specified path."""
        supervisor_zip_url = GitHubUtils.get_asset_url(cls._SUPERVISOR_GITHUB_USER,
                                                       cls._SUPERVISOR_GITHUB_REPO,
                                                       asset_name,
                                                       version)
        with open(supervisor_zip_path, "wb") as thezip:
            thezip.write(request.get_file(supervisor_zip_url))
        return supervisor_zip_path

    @classmethod
    def is_supervisor_asset_cached(cls, asset_name: str, supervisor_version: str) -> Tuple[bool, str]:
        """Check if specified supervisor asset is cached."""
        supervisor_path = FileUtils.join_paths(cls._SUPERVISOR_CACHE_DIR, supervisor_version)
        supervisor_zip_path = FileUtils.join_paths(supervisor_path, asset_name)
        try:
            # The file must exist and be more that 1MB
            if os.path.isfile(supervisor_zip_path) and os.path.getsize(supervisor_zip_path) > 1048576:
                return True, supervisor_zip_path
            elif not os.path.exists(supervisor_path):
                os.makedirs(supervisor_path)
        except Exception as ex:
            logger.warning('Error checking asset file in cache: %s' % ex)
        return False, supervisor_zip_path

    @classmethod
    def is_supervisor_cached(cls, supervisor_version: str) -> Tuple[bool, str]:
        """Check if supervisor source is cached."""
        return SupervisorUtils.is_supervisor_asset_cached(cls._SUPERVISOR_SOURCE_NAME, supervisor_version)
import os
import scar.exceptions as excp
import scar.logger as logger
import scar.providers.aws.response as response_parser
from typing import Dict
from copy import deepcopy
from scar.cmdtemplate import Commands
from scar.providers.aws.apigateway import APIGateway
from scar.providers.aws.batchfunction import Batch
from scar.providers.aws.cloudwatchlogs import CloudWatchLogs
from scar.providers.aws.iam import IAM
from scar.providers.aws.lambdafunction import Lambda
from scar.providers.aws.resourcegroups import ResourceGroups
from scar.providers.aws.s3 import S3, get_bucket_and_folders
from scar.providers.aws.validators import AWSValidator
from scar.utils import StrUtils, FileUtils, SupervisorUtils
# Copyright (C) GRyCAP - I3M - UPV
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Module with classes and methods used to manage the AWS tools."""

# from scar.providers.aws.properties import AwsProperties, ScarProperties

_ACCOUNT_ID_REGEX = r'\d{12}'


def _get_owner(resources_info: Dict):
    return IAM(resources_info).get_user_name_or_id()


def _check_function_defined(resources_info: Dict):
    if Lambda(resources_info).find_function():
        raise excp.FunctionExistsError(function_name=resources_info.get('lambda', {}).get('name', ''))


def _check_function_not_defined(resources_info: Dict):
    if not Lambda(resources_info).find_function():
        raise excp.FunctionNotFoundError(function_name=resources_info.get('lambda', {}).get('name', ''))


def _choose_function(aws_resources: Dict) -> int:
    function_names = [resources_info.get('lambda').get('name') for resources_info in aws_resources]
    print("Please choose a function:")
    print("0) Apply to all")
    for idx, element in enumerate(function_names):
        print(f"{idx+1}) {element}")
    i = input("Enter number: ")
    if 0 <= int(i) <= len(function_names):
        return int(i) - 1
    return None


def _get_all_functions(resources_info: Dict):
    arn_list = ResourceGroups(resources_info).get_resource_arn_list(IAM(resources_info).get_user_name_or_id())
    return Lambda(resources_info).get_all_functions(arn_list)


def _check_preheat_function(resources_info: Dict):
    if resources_info.get('lambda').get('preheat', False):
        Lambda(resources_info).preheat_function()

############################################
###          ADD EXTRA PROPERTIES        ###
############################################


def _add_extra_aws_properties(scar: Dict, aws_resources: Dict) -> None:
    for resources_info in aws_resources:
        _add_tags(resources_info)
        _add_handler(resources_info)
        _add_account_id(resources_info)
        add_output(scar)
        _add_config_file_path(scar, resources_info)


def _add_tags(resources_info: Dict):
    resources_info['lambda']['tags'] = {"createdby": "scar", "owner": _get_owner(resources_info)}


def _add_account_id(resources_info: Dict):

    resources_info['iam']['account_id'] = StrUtils.find_expression(resources_info['iam']['role'], _ACCOUNT_ID_REGEX)