def _plan_sqseventsource(self, resource): # type: (models.SQSEventSource) -> Sequence[InstructionMsg] queue_arn_varname = '%s_queue_arn' % resource.resource_name uuid_varname = '%s_uuid' % resource.resource_name function_arn = Variable('%s_lambda_arn' % resource.lambda_function.resource_name) instruction_for_queue_arn = [ models.BuiltinFunction( 'parse_arn', [function_arn], output_var='parsed_lambda_arn', ), models.JPSearch('account_id', input_var='parsed_lambda_arn', output_var='account_id'), models.JPSearch('region', input_var='parsed_lambda_arn', output_var='region_name'), models.StoreValue( name=queue_arn_varname, value=StringFormat( 'arn:aws:sqs:{region_name}:{account_id}:%s' % (resource.queue), ['region_name', 'account_id'], ), ), ] # type: List[InstructionMsg] if self._remote_state.resource_exists(resource): deployed = self._remote_state.resource_deployed_values(resource) uuid = deployed['event_uuid'] return instruction_for_queue_arn + [ models.APICall(method_name='update_sqs_event_source', params={ 'event_uuid': uuid, 'batch_size': resource.batch_size }), models.RecordResourceValue( resource_type='sqs_event', resource_name=resource.resource_name, name='queue_arn', value=deployed['queue_arn'], ), models.RecordResourceValue( resource_type='sqs_event', resource_name=resource.resource_name, name='event_uuid', value=uuid, ), models.RecordResourceValue( resource_type='sqs_event', resource_name=resource.resource_name, name='queue', value=resource.queue, ), models.RecordResourceValue( resource_type='sqs_event', resource_name=resource.resource_name, name='lambda_arn', value=deployed['lambda_arn'], ), ] return instruction_for_queue_arn + [ (models.APICall( method_name='create_sqs_event_source', params={ 'queue_arn': Variable(queue_arn_varname), 'batch_size': resource.batch_size, 'function_name': function_arn }, output_var=uuid_varname, ), 'Subscribing %s to SQS queue %s\n' % (resource.lambda_function.function_name, resource.queue)), models.RecordResourceVariable( resource_type='sqs_event', resource_name=resource.resource_name, name='queue_arn', variable_name=queue_arn_varname, ), # We record this because this is what's used to unsubscribe # lambda to the SQS queue. models.RecordResourceVariable( resource_type='sqs_event', resource_name=resource.resource_name, name='event_uuid', variable_name=uuid_varname, ), models.RecordResourceValue( resource_type='sqs_event', resource_name=resource.resource_name, name='queue', value=resource.queue, ), models.RecordResourceVariable( resource_type='sqs_event', resource_name=resource.resource_name, name='lambda_arn', variable_name=function_arn.name, ), ]
def _plan_websocketapi(self, resource): # type: (models.WebsocketAPI) -> Sequence[InstructionMsg] configs = self._create_websocket_function_configs(resource) routes = resource.routes # Which lambda function we use here does not matter. We are only using # it to find the account id and the region. lambda_arn_var = list(configs.values())[0]['lambda_arn_var'] shared_plan_preamble = [ # The various API gateway API calls need # to know the region name and account id so # we'll take care of that up front and store # them in variables. models.BuiltinFunction( 'parse_arn', [lambda_arn_var], output_var='parsed_lambda_arn', ), models.JPSearch('account_id', input_var='parsed_lambda_arn', output_var='account_id'), models.JPSearch('region', input_var='parsed_lambda_arn', output_var='region_name'), ] # type: List[InstructionMsg] # There's also a set of instructions that are needed # at the end of deploying a websocket API that apply to both # the update and create case. shared_plan_epilogue = [ models.StoreValue( name='websocket_api_url', value=StringFormat( 'wss://{websocket_api_id}.execute-api.{region_name}' '.amazonaws.com/%s/' % resource.api_gateway_stage, ['websocket_api_id', 'region_name'], ), ), models.RecordResourceVariable( resource_type='websocket_api', resource_name=resource.resource_name, name='websocket_api_url', variable_name='websocket_api_url', ), models.RecordResourceVariable( resource_type='websocket_api', resource_name=resource.resource_name, name='websocket_api_id', variable_name='websocket_api_id', ), ] # type: List[InstructionMsg] shared_plan_epilogue += [ models.APICall( method_name='add_permission_for_apigateway_v2', params={ 'function_name': function_config['name'], 'region_name': Variable('region_name'), 'account_id': Variable('account_id'), 'api_id': Variable('websocket_api_id') }, ) for function_config in configs.values() ] main_plan = [] # type: List[InstructionMsg] if not self._remote_state.resource_exists(resource): # The resource does not exist, we create it in full here. main_plan += [ (models.APICall( method_name='create_websocket_api', params={'name': resource.name}, output_var='websocket_api_id', ), "Creating websocket api: %s\n" % resource.name), models.StoreValue( name='routes', value=[], ), ] main_plan += self._inject_websocket_integrations(configs) for route_key in routes: main_plan += [self._create_route_for_key(route_key)] main_plan += [ models.APICall( method_name='deploy_websocket_api', params={ 'api_id': Variable('websocket_api_id'), }, output_var='deployment-id', ), models.APICall(method_name='create_stage', params={ 'api_id': Variable('websocket_api_id'), 'stage_name': resource.api_gateway_stage, 'deployment_id': Variable('deployment-id'), }), ] else: # Already exists. Need to sync up the routes, the easiest way to do # this is to delete them and their integrations and re-create them. # They will not work if the lambda function changes from under # them, and the logic for detecting that and making just the needed # changes is complex. There is an integration test to ensure there # no dropped messages during a redeployment. deployed = self._remote_state.resource_deployed_values(resource) main_plan += [ models.StoreValue(name='websocket_api_id', value=deployed['websocket_api_id']), models.APICall( method_name='get_websocket_routes', params={'api_id': Variable('websocket_api_id')}, output_var='routes', ), models.APICall( method_name='delete_websocket_routes', params={ 'api_id': Variable('websocket_api_id'), 'routes': Variable('routes'), }, ), models.APICall(method_name='get_websocket_integrations', params={ 'api_id': Variable('websocket_api_id'), }, output_var='integrations'), models.APICall(method_name='delete_websocket_integrations', params={ 'api_id': Variable('websocket_api_id'), 'integrations': Variable('integrations'), }) ] main_plan += self._inject_websocket_integrations(configs) for route_key in routes: main_plan += [self._create_route_for_key(route_key)] return shared_plan_preamble + main_plan + shared_plan_epilogue
def _plan_snslambdasubscription(self, resource): # type: (models.SNSLambdaSubscription) -> Sequence[InstructionMsg] function_arn = Variable('%s_lambda_arn' % resource.lambda_function.resource_name) topic_arn_varname = '%s_topic_arn' % resource.resource_name subscribe_varname = '%s_subscription_arn' % resource.resource_name instruction_for_topic_arn = [] # type: List[InstructionMsg] if resource.topic.startswith('arn:aws:sns:'): instruction_for_topic_arn += [ models.StoreValue( name=topic_arn_varname, value=resource.topic, ) ] else: # To keep the user API simple, we only require the topic # name and not the ARN. However, the APIs require the topic # ARN so we need to reconstruct it here in the planner. instruction_for_topic_arn += [ models.BuiltinFunction( 'parse_arn', [function_arn], output_var='parsed_lambda_arn', ), models.JPSearch('account_id', input_var='parsed_lambda_arn', output_var='account_id'), models.JPSearch('region', input_var='parsed_lambda_arn', output_var='region_name'), models.StoreValue( name=topic_arn_varname, value=StringFormat( 'arn:aws:sns:{region_name}:{account_id}:%s' % (resource.topic), ['region_name', 'account_id'], ), ), ] if self._remote_state.resource_exists(resource): # Given there's nothing about an SNS subscription you can # configure for now, if the resource exists, we don't do # anything. The resource sweeper will verify that if the # subscription doesn't actually apply that we should unsubscribe # from the topic. deployed = self._remote_state.resource_deployed_values(resource) subscription_arn = deployed['subscription_arn'] return instruction_for_topic_arn + [ models.RecordResourceValue( resource_type='sns_event', resource_name=resource.resource_name, name='topic', value=resource.topic, ), models.RecordResourceVariable( resource_type='sns_event', resource_name=resource.resource_name, name='lambda_arn', variable_name=function_arn.name, ), models.RecordResourceValue( resource_type='sns_event', resource_name=resource.resource_name, name='subscription_arn', value=subscription_arn, ), models.RecordResourceVariable( resource_type='sns_event', resource_name=resource.resource_name, name='topic_arn', variable_name=topic_arn_varname, ), ] return instruction_for_topic_arn + [ models.APICall( method_name='add_permission_for_sns_topic', params={ 'topic_arn': Variable(topic_arn_varname), 'function_arn': function_arn }, ), (models.APICall( method_name='subscribe_function_to_topic', params={ 'topic_arn': Variable(topic_arn_varname), 'function_arn': function_arn }, output_var=subscribe_varname, ), 'Subscribing %s to SNS topic %s\n' % (resource.lambda_function.function_name, resource.topic)), models.RecordResourceValue( resource_type='sns_event', resource_name=resource.resource_name, name='topic', value=resource.topic, ), models.RecordResourceVariable( resource_type='sns_event', resource_name=resource.resource_name, name='lambda_arn', variable_name=function_arn.name, ), models.RecordResourceVariable( resource_type='sns_event', resource_name=resource.resource_name, name='subscription_arn', variable_name=subscribe_varname, ), models.RecordResourceVariable( resource_type='sns_event', resource_name=resource.resource_name, name='topic_arn', variable_name=topic_arn_varname, ), ]
def _plan_lambdafunction(self, resource): # type: (models.LambdaFunction) -> Sequence[InstructionMsg] role_arn = self._get_role_arn(resource.role) # Make mypy happy, it complains if we don't "declare" this upfront. params = {} # type: Dict[str, Any] varname = '%s_lambda_arn' % resource.resource_name # Not sure the best way to express this via mypy, but we know # that in the build stage we replace the deployment package # name with the actual filename generated from the pip # packager. For now we resort to a cast. filename = cast(str, resource.deployment_package.filename) if resource.reserved_concurrency is None: concurrency_api_call = models.APICall( method_name='delete_function_concurrency', params={'function_name': resource.function_name}, output_var='reserved_concurrency_result') else: concurrency = resource.reserved_concurrency concurrency_api_call = ( models.APICall(method_name='put_function_concurrency', params={ 'function_name': resource.function_name, 'reserved_concurrent_executions': concurrency, }, output_var='reserved_concurrency_result'), "Updating lambda function concurrency limit: %s\n" % resource.function_name) api_calls = [] # type: List[InstructionMsg] if not self._remote_state.resource_exists(resource): params = { 'function_name': resource.function_name, 'role_arn': role_arn, 'zip_contents': self._osutils.get_file_contents(filename, binary=True), 'runtime': resource.runtime, 'handler': resource.handler, 'environment_variables': resource.environment_variables, 'tags': resource.tags, 'timeout': resource.timeout, 'memory_size': resource.memory_size, 'security_group_ids': resource.security_group_ids, 'subnet_ids': resource.subnet_ids, 'layers': resource.layers } api_calls.extend([(models.APICall( method_name='create_function', params=params, output_var=varname, ), "Creating lambda function: %s\n" % resource.function_name), models.RecordResourceVariable( resource_type='lambda_function', resource_name=resource.resource_name, name='lambda_arn', variable_name=varname, )]) else: # TODO: Consider a smarter diff where we check if we even need # to do an update() API call. params = { 'function_name': resource.function_name, 'role_arn': role_arn, 'zip_contents': self._osutils.get_file_contents(filename, binary=True), 'runtime': resource.runtime, 'environment_variables': resource.environment_variables, 'tags': resource.tags, 'timeout': resource.timeout, 'memory_size': resource.memory_size, 'security_group_ids': resource.security_group_ids, 'subnet_ids': resource.subnet_ids, 'layers': resource.layers } api_calls.extend([(models.APICall( method_name='update_function', params=params, output_var='update_function_result', ), "Updating lambda function: %s\n" % resource.function_name), models.JPSearch( 'FunctionArn', input_var='update_function_result', output_var=varname, ), models.RecordResourceVariable( resource_type='lambda_function', resource_name=resource.resource_name, name='lambda_arn', variable_name=varname, )]) api_calls.append(concurrency_api_call) return api_calls
def _plan_restapi(self, resource): # type: (models.RestAPI) -> Sequence[InstructionMsg] function = resource.lambda_function function_name = function.function_name varname = '%s_lambda_arn' % function.resource_name lambda_arn_var = Variable(varname) # There's a set of shared instructions that are needed # in both the update as well as the initial create case. # That's what this shared_plan_premable is for. shared_plan_preamble = [ # The various API gateway API calls need # to know the region name and account id so # we'll take care of that up front and store # them in variables. models.BuiltinFunction( 'parse_arn', [lambda_arn_var], output_var='parsed_lambda_arn', ), models.JPSearch('account_id', input_var='parsed_lambda_arn', output_var='account_id'), models.JPSearch('region', input_var='parsed_lambda_arn', output_var='region_name'), # The swagger doc uses the 'api_handler_lambda_arn' # var name so we need to make sure we populate this variable # before importing the rest API. models.CopyVariable(from_var=varname, to_var='api_handler_lambda_arn'), ] # type: List[InstructionMsg] # There's also a set of instructions that are needed # at the end of deploying a rest API that apply to both # the update and create case. shared_plan_epilogue = [ models.APICall( method_name='add_permission_for_apigateway', params={ 'function_name': function_name, 'region_name': Variable('region_name'), 'account_id': Variable('account_id'), 'rest_api_id': Variable('rest_api_id') }, ), models.StoreValue( name='rest_api_url', value=StringFormat( 'https://{rest_api_id}.execute-api.{region_name}' '.amazonaws.com/%s/' % resource.api_gateway_stage, ['rest_api_id', 'region_name'], ), ), models.RecordResourceVariable( resource_type='rest_api', resource_name=resource.resource_name, name='rest_api_url', variable_name='rest_api_url', ), ] # type: List[InstructionMsg] for auth in resource.authorizers: shared_plan_epilogue.append( models.APICall( method_name='add_permission_for_apigateway', params={ 'function_name': auth.function_name, 'region_name': Variable('region_name'), 'account_id': Variable('account_id'), 'rest_api_id': Variable('rest_api_id') }, )) if not self._remote_state.resource_exists(resource): plan = shared_plan_preamble + [ (models.APICall( method_name='import_rest_api', params={'swagger_document': resource.swagger_doc}, output_var='rest_api_id', ), "Creating Rest API\n"), models.RecordResourceVariable( resource_type='rest_api', resource_name=resource.resource_name, name='rest_api_id', variable_name='rest_api_id', ), models.APICall( method_name='deploy_rest_api', params={ 'rest_api_id': Variable('rest_api_id'), 'api_gateway_stage': resource.api_gateway_stage }, ), ] + shared_plan_epilogue else: deployed = self._remote_state.resource_deployed_values(resource) plan = shared_plan_preamble + [ models.StoreValue(name='rest_api_id', value=deployed['rest_api_id']), models.RecordResourceVariable( resource_type='rest_api', resource_name=resource.resource_name, name='rest_api_id', variable_name='rest_api_id', ), (models.APICall( method_name='update_api_from_swagger', params={ 'rest_api_id': Variable('rest_api_id'), 'swagger_document': resource.swagger_doc, }, ), "Updating rest API\n"), models.APICall( method_name='deploy_rest_api', params={ 'rest_api_id': Variable('rest_api_id'), 'api_gateway_stage': resource.api_gateway_stage }, ), models.APICall( method_name='add_permission_for_apigateway', params={ 'function_name': function_name, 'region_name': Variable('region_name'), 'account_id': Variable('account_id'), 'rest_api_id': Variable('rest_api_id') }, ), ] + shared_plan_epilogue return plan
def _plan_lambdafunction(self, resource): # type: (models.LambdaFunction) -> Sequence[_INSTRUCTION_MSG] role_arn = self._get_role_arn(resource.role) # Make mypy happy, it complains if we don't "declare" this upfront. params = {} # type: Dict[str, Any] varname = '%s_lambda_arn' % resource.resource_name if not self._remote_state.resource_exists(resource): params = { 'function_name': resource.function_name, 'role_arn': role_arn, 'zip_contents': self._osutils.get_file_contents( resource.deployment_package.filename, binary=True), 'runtime': resource.runtime, 'handler': resource.handler, 'environment_variables': resource.environment_variables, 'tags': resource.tags, 'timeout': resource.timeout, 'memory_size': resource.memory_size, 'security_group_ids': resource.security_group_ids, 'subnet_ids': resource.subnet_ids, } return [(models.APICall( method_name='create_function', params=params, output_var=varname, ), "Creating lambda function: %s\n" % resource.function_name), models.RecordResourceVariable( resource_type='lambda_function', resource_name=resource.resource_name, name='lambda_arn', variable_name=varname, )] # TODO: Consider a smarter diff where we check if we even need # to do an update() API call. params = { 'function_name': resource.function_name, 'role_arn': role_arn, 'zip_contents': self._osutils.get_file_contents( resource.deployment_package.filename, binary=True), 'runtime': resource.runtime, 'environment_variables': resource.environment_variables, 'tags': resource.tags, 'timeout': resource.timeout, 'memory_size': resource.memory_size, 'security_group_ids': resource.security_group_ids, 'subnet_ids': resource.subnet_ids, } return [(models.APICall( method_name='update_function', params=params, output_var='update_function_result', ), "Updating lambda function: %s\n" % resource.function_name), models.JPSearch( 'FunctionArn', input_var='update_function_result', output_var=varname, ), models.RecordResourceVariable( resource_type='lambda_function', resource_name=resource.resource_name, name='lambda_arn', variable_name=varname, )]
def test_can_update_rest_api(self): function = create_function_resource('function_name') rest_api = models.RestAPI( resource_name='rest_api', swagger_doc={'swagger': '2.0'}, api_gateway_stage='api', lambda_function=function, ) self.remote_state.declare_resource_exists(rest_api) self.remote_state.deployed_values['rest_api'] = { 'rest_api_id': 'my_rest_api_id', } plan = self.determine_plan(rest_api) self.assert_loads_needed_variables(plan) assert plan[4:] == [ models.StoreValue(name='rest_api_id', value='my_rest_api_id'), models.RecordResourceVariable( resource_type='rest_api', resource_name='rest_api', name='rest_api_id', variable_name='rest_api_id', ), models.APICall( method_name='update_api_from_swagger', params={ 'rest_api_id': Variable('rest_api_id'), 'swagger_document': { 'swagger': '2.0' }, }, ), models.APICall( method_name='deploy_rest_api', params={ 'rest_api_id': Variable('rest_api_id'), 'api_gateway_stage': 'api' }, ), models.APICall( method_name='add_permission_for_apigateway_if_needed', params={ 'function_name': 'appname-dev-function_name', 'region_name': Variable('region_name'), 'account_id': Variable('account_id'), 'rest_api_id': Variable('rest_api_id') }, ), models.APICall( method_name='add_permission_for_apigateway_if_needed', params={ 'rest_api_id': Variable("rest_api_id"), 'region_name': Variable("region_name"), 'account_id': Variable("account_id"), 'function_name': 'appname-dev-function_name' }, output_var=None), models.StoreValue( name='rest_api_url', value=StringFormat( 'https://{rest_api_id}.execute-api.{region_name}' '.amazonaws.com/api/', ['rest_api_id', 'region_name'], ), ), models.RecordResourceVariable(resource_type='rest_api', resource_name='rest_api', name='rest_api_url', variable_name='rest_api_url'), ]