def _inject_websocket_integrations(self, configs): # type: (Dict[str, Any]) -> Sequence[InstructionMsg] instructions = [] # type: List[InstructionMsg] for key, config in configs.items(): instructions.append( models.StoreValue( name='websocket-%s-integration-lambda-path' % key, value=StringFormat( 'arn:aws:apigateway:{region_name}:lambda:path/' '2015-03-31/functions/arn:aws:lambda:{region_name}:' '{account_id}:function:%s/' 'invocations' % config['name'], ['region_name', 'account_id'], ), ), ) instructions.append( models.APICall( method_name='create_websocket_integration', params={ 'api_id': Variable('websocket_api_id'), 'lambda_function': Variable('websocket-%s-integration-lambda-path' % key), 'handler_type': key, }, output_var='%s-integration-id' % key, ), ) return instructions
def test_can_update_managed_role(self): role = models.ManagedIAMRole( resource_name='resource_name', role_name='myrole', trust_policy={}, policy=models.AutoGenIAMPolicy(document={'role': 'policy'}), ) self.remote_state.declare_resource_exists(role, role_arn='myrole:arn') plan = self.determine_plan(role) assert plan[0] == models.StoreValue(name='myrole_role_arn', value='myrole:arn') self.assert_apicall_equals( plan[1], models.APICall( method_name='put_role_policy', params={ 'role_name': 'myrole', 'policy_name': 'myrole', 'policy_document': { 'role': 'policy' } }, )) assert plan[-2].variable_name == 'myrole_role_arn' assert plan[-1].value == 'myrole' assert list(self.last_plan.messages.values()) == [ 'Updating policy for IAM role: myrole\n' ]
def _plan_managediamrole(self, resource): # type: (models.ManagedIAMRole) -> Sequence[InstructionMsg] document = resource.policy.document role_exists = self._remote_state.resource_exists(resource) varname = '%s_role_arn' % resource.role_name if not role_exists: return [(models.APICall( method_name='create_role', params={ 'name': resource.role_name, 'trust_policy': resource.trust_policy, 'policy': document }, output_var=varname, ), "Creating IAM role: %s\n" % resource.role_name), models.RecordResourceVariable( resource_type='iam_role', resource_name=resource.resource_name, name='role_arn', variable_name=varname, ), models.RecordResourceValue( resource_type='iam_role', resource_name=resource.resource_name, name='role_name', value=resource.role_name, )] role_arn = self._remote_state.resource_deployed_values( resource)['role_arn'] return [ models.StoreValue(name=varname, value=role_arn), (models.APICall( method_name='put_role_policy', params={ 'role_name': resource.role_name, 'policy_name': resource.role_name, 'policy_document': document }, ), "Updating policy for IAM role: %s\n" % resource.role_name), models.RecordResourceVariable( resource_type='iam_role', resource_name=resource.resource_name, name='role_arn', variable_name=varname, ), models.RecordResourceValue( resource_type='iam_role', resource_name=resource.resource_name, name='role_name', value=resource.role_name, ) ]
def test_can_plan_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, ) plan = self.determine_plan(rest_api) self.assert_loads_needed_variables(plan) assert plan[4:] == [ models.APICall( method_name='import_rest_api', params={'swagger_document': { 'swagger': '2.0' }}, output_var='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='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.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'), ] assert list( self.last_plan.messages.values()) == ['Creating Rest API\n']
def test_can_update_file_based_policy(self): role = models.ManagedIAMRole( resource_name='resource_name', role_name='myrole', trust_policy={}, policy=models.FileBasedIAMPolicy(filename='foo.json', document={'iam': 'policy'}), ) self.remote_state.declare_resource_exists(role, role_arn='myrole:arn') plan = self.determine_plan(role) assert plan[0] == models.StoreValue(name='myrole_role_arn', value='myrole:arn') self.assert_apicall_equals( plan[1], models.APICall( method_name='put_role_policy', params={ 'role_name': 'myrole', 'policy_name': 'myrole', 'policy_document': { 'iam': 'policy' } }, ))
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_patch_ops = [{ 'op': 'replace', 'path': '/minimumCompressionSize', 'value': resource.minimum_compression }] # type: List[Dict] shared_plan_epilogue = [ models.APICall(method_name='update_rest_api', params={ 'rest_api_id': Variable('rest_api_id'), 'patch_operations': shared_plan_patch_ops }), 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.APICall( method_name='deploy_rest_api', params={ 'rest_api_id': Variable('rest_api_id'), 'api_gateway_stage': resource.api_gateway_stage }, ), 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, 'endpoint_type': resource.endpoint_type }, 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', ), ] else: deployed = self._remote_state.resource_deployed_values(resource) shared_plan_epilogue.insert( 0, models.APICall(method_name='get_rest_api', params={'rest_api_id': Variable('rest_api_id')}, output_var='rest_api')) shared_plan_patch_ops.append({ 'op': 'replace', 'path': StringFormat( '/endpointConfiguration/types/%s' % ('{rest_api[endpointConfiguration][types][0]}'), ['rest_api']), 'value': resource.endpoint_type }) 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"), ] plan.extend(shared_plan_epilogue) return plan
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_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_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 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'), ]