def test_stack_template_url(self): """Test stack template url.""" context = mock_context("mynamespace") blueprint = MockBlueprint(name="myblueprint", context=context) region = "us-east-1" endpoint = "https://example.com" session = get_session(region) provider = Provider(session) action = BaseAction( context=context, provider_builder=MockProviderBuilder(provider, region=region), ) with patch( "runway.cfngin.actions.base.get_s3_endpoint", autospec=True, return_value=endpoint, ): self.assertEqual( action.stack_template_url(blueprint), "%s/%s/stack_templates/%s/%s-%s.json" % ( endpoint, "stacker-mynamespace", "mynamespace-myblueprint", "myblueprint", MOCK_VERSION, ), )
def setUp(self): """Run before tests.""" self.region = "us-east-1" self.session = get_session(self.region) self.provider = Provider(self.session) self.config_no_persist = { "stacks": [{ "name": "stack1" }, { "name": "stack2", "requires": ["stack1"] }] } self.config_persist = { "persistent_graph_key": "test.json", "stacks": [{ "name": "stack1" }, { "name": "stack2", "requires": ["stack1"] }], }
def test_ensure_cfn_bucket_does_not_exist_us_west(self): """Test ensure cfn bucket does not exist us west.""" session = get_session("us-west-1") provider = Provider(session) action = BaseAction( context=mock_context("mynamespace"), provider_builder=MockProviderBuilder(provider, region="us-west-1"), ) stubber = Stubber(action.s3_conn) stubber.add_client_error( "head_bucket", service_error_code="NoSuchBucket", service_message="Not Found", http_status_code=404, ) stubber.add_response( "create_bucket", service_response={}, expected_params={ "Bucket": ANY, "CreateBucketConfiguration": { "LocationConstraint": "us-west-1" }, }, ) with stubber: action.ensure_cfn_bucket()
def update( context, # pylint: disable=unused-argument provider, **kwargs): # noqa: E124 # type: (Context, BaseProvider, Optional[Dict[str, Any]]) -> bool """Update the callback urls for the User Pool Client. Required to match the redirect_uri being sent which contains our distribution and alternate domain names. Args: context (:class:`runway.cfngin.context.Context`): The context instance. provider (:class:`runway.cfngin.providers.base.BaseProvider`): The provider instance Keyword Args: alternate_domains (List[str]): A list of any alternate domains that need to be listed with the primary distribution domain redirect_path_sign_in (str): The redirect path after sign in redirect_path_sign_out (str): The redirect path after sign out oauth_scopes (List[str]): A list of all available validation scopes for oauth """ session = get_session(provider.region) cognito_client = session.client('cognito-idp') # Combine alternate domains with main distribution redirect_domains = kwargs['alternate_domains'] + [ 'https://' + kwargs['distribution_domain'] ] # Create a list of all domains with their redirect paths redirect_uris_sign_in = [ "%s%s" % (domain, kwargs['redirect_path_sign_in']) for domain in redirect_domains ] redirect_uris_sign_out = [ "%s%s" % (domain, kwargs['redirect_path_sign_out']) for domain in redirect_domains ] # Update the user pool client try: cognito_client.update_user_pool_client( AllowedOAuthScopes=kwargs['oauth_scopes'], AllowedOAuthFlows=['code'], SupportedIdentityProviders=kwargs['supported_identity_providers'], AllowedOAuthFlowsUserPoolClient=True, ClientId=kwargs['client_id'], CallbackURLs=redirect_uris_sign_in, LogoutURLs=redirect_uris_sign_out, UserPoolId=context.hook_data['aae_user_pool_id_retriever']['id'], ) return True except Exception as err: # pylint: disable=broad-except LOGGER.error('Was not able to update the callback urls on ' 'the user pool client') LOGGER.error(err) return False
def setUp(self): """Run before tests.""" region = "us-east-1" self.session = get_session(region=region) self.provider = Provider(self.session, interactive=True, recreate_failed=True) self.stubber = Stubber(self.provider.cloudformation)
def setUp(self) -> None: """Run before tests.""" self.context = self._get_context() self.session = get_session(region=None) self.provider = Provider(self.session, interactive=False, recreate_failed=False) provider_builder = MockProviderBuilder(provider=self.provider) self.deploy_action = deploy.Action( self.context, provider_builder=provider_builder, cancel=MockThreadingEvent(), # type: ignore ) self.stack = MagicMock() self.stack.region = None self.stack.name = "vpc" self.stack.fqn = "vpc" self.stack.blueprint.rendered = "{}" self.stack.locked = False self.stack_status = None plan = cast( Plan, self.deploy_action._Action__generate_plan()) # type: ignore self.step = plan.steps[0] self.step.stack = self.stack def patch_object(*args: Any, **kwargs: Any) -> None: mock_object = patch.object(*args, **kwargs) self.addCleanup(mock_object.stop) mock_object.start() def get_stack(name: str, *_args: Any, **_kwargs: Any) -> Dict[str, Any]: if name != self.stack.name or not self.stack_status: raise StackDoesNotExist(name) return { "StackName": self.stack.name, "StackStatus": self.stack_status, "Outputs": [], "Tags": [], } def get_events(name: str, *_args: Any, **_kwargs: Any) -> List[Dict[str, str]]: return [{ "ResourceStatus": "ROLLBACK_IN_PROGRESS", "ResourceStatusReason": "CFN fail", }] patch_object(self.provider, "get_stack", side_effect=get_stack) patch_object(self.provider, "update_stack") patch_object(self.provider, "create_stack") patch_object(self.provider, "destroy_stack") patch_object(self.provider, "get_events", side_effect=get_events) patch_object(self.deploy_action, "s3_stack_push")
def get_principal_arn(provider): """Return ARN of current session principle.""" # looking up caller identity session = get_session(provider.region) sts_client = session.client('sts') caller_identity_arn = sts_client.get_caller_identity()['Arn'] if caller_identity_arn.split(':')[2] == 'iam' and ( caller_identity_arn.split(':')[5].startswith('user/')): return caller_identity_arn # user arn return assumed_role_to_principle(caller_identity_arn)
def get_principal_arn(provider): """Return ARN of current session principle.""" # looking up caller identity session = get_session(provider.region) sts_client = session.client("sts") caller_identity_arn = sts_client.get_caller_identity()["Arn"] if caller_identity_arn.split(":")[2] == "iam" and ( caller_identity_arn.split(":")[5].startswith("user/")): return caller_identity_arn # user arn return assumed_role_to_principle(caller_identity_arn)
def update( context, # type: Context # pylint: disable=unused-import provider, # type: BaseProvider **kwargs # type: Optional[Dict[str, Any]] ): # noqa: E124 # type: (...) -> Union[Dict[str, Any], bool] """Retrieve/Update the domain name of the specified client. A domain name is required in order to make authorization and token requests. This prehook ensures we have one available, and if not we create one based on the user pool and client ids. Args: context (:class:`runway.cfngin.context.Context`): The context instance. provider (:class:`runway.cfngin.providers.base.BaseProvider`): The provider instance Keyword Args: user_pool_id (str): The ID of the Cognito User Pool client_id (str): The ID of the Cognito User Pool Client """ session = get_session(provider.region) cognito_client = session.client('cognito-idp') context_dict = {} user_pool_id = context.hook_data['aae_user_pool_id_retriever']['id'] client_id = kwargs['client_id'] user_pool = cognito_client.describe_user_pool( UserPoolId=user_pool_id).get('UserPool') (user_pool_region, user_pool_hash) = user_pool_id.split('_') domain_prefix = user_pool.get('CustomDomain') or user_pool.get('Domain') # Return early if we already have a domain if domain_prefix: context_dict['domain'] = get_user_pool_domain(domain_prefix, user_pool_region) return context_dict try: domain_prefix = ('%s-%s' % (user_pool_hash, client_id)).lower() cognito_client.create_user_pool_domain(Domain=domain_prefix, UserPoolId=user_pool_id) context_dict['domain'] = get_user_pool_domain(domain_prefix, user_pool_region) return context_dict except Exception as err: # pylint: disable=broad-except LOGGER.error('Could not update user pool domain for user pool id %s.', user_pool_id) LOGGER.error(err) return False
def setUp(self): """Run before tests.""" self.context = self._get_context() self.session = get_session(region=None) self.provider = Provider(self.session, interactive=False, recreate_failed=False) provider_builder = MockProviderBuilder(self.provider) self.build_action = build.Action(self.context, provider_builder=provider_builder, cancel=MockThreadingEvent()) self.stack = MagicMock() self.stack.region = None self.stack.name = 'vpc' self.stack.fqn = 'vpc' self.stack.blueprint.rendered = '{}' self.stack.locked = False self.stack_status = None plan = self.build_action._Action__generate_plan() self.step = plan.steps[0] self.step.stack = self.stack def patch_object(*args, **kwargs): mock_object = patch.object(*args, **kwargs) self.addCleanup(mock_object.stop) mock_object.start() def get_stack(name, *args, **kwargs): if name != self.stack.name or not self.stack_status: raise StackDoesNotExist(name) return { 'StackName': self.stack.name, 'StackStatus': self.stack_status, 'Outputs': [], 'Tags': [] } def get_events(name, *args, **kwargs): return [{ 'ResourceStatus': 'ROLLBACK_IN_PROGRESS', 'ResourceStatusReason': 'CFN fail' }] patch_object(self.provider, 'get_stack', side_effect=get_stack) patch_object(self.provider, 'update_stack') patch_object(self.provider, 'create_stack') patch_object(self.provider, 'destroy_stack') patch_object(self.provider, 'get_events', side_effect=get_events) patch_object(self.build_action, "s3_stack_push")
def test_ensure_cfn_bucket_exists(self): """Test ensure cfn bucket exists.""" session = get_session("us-east-1") provider = Provider(session) action = BaseAction( context=mock_context("mynamespace"), provider_builder=MockProviderBuilder(provider), ) stubber = Stubber(action.s3_conn) stubber.add_response("head_bucket", service_response={}, expected_params={"Bucket": ANY}) with stubber: action.ensure_cfn_bucket()
def test_ensure_cfn_forbidden(self): """Test ensure cfn forbidden.""" session = get_session("us-west-1") provider = Provider(session) action = BaseAction(context=mock_context("mynamespace"), provider_builder=MockProviderBuilder(provider)) stubber = Stubber(action.s3_conn) stubber.add_client_error( "head_bucket", service_error_code="AccessDenied", service_message="Forbidden", http_status_code=403, ) with stubber: with self.assertRaises(botocore.exceptions.ClientError): action.ensure_cfn_bucket()
def execute( context, # type: Context # pylint: disable=unused-argument provider, # type: BaseProvider **kwargs # type: Optional[Dict[str, Any]] ): # noqa: E124 # type: (...) -> Union[Dict[str, Any], bool] """Execute the cleanup process. A StateMachine will be executed that stays active after the main and dependency stacks have been deleted. This will keep attempting to delete the Replicated functions that were created as part of the main stack. Once it has deleted all the Lambdas supplied it will self destruct its own stack. Args: context (:class:`runway.cfngin.context.Context`): The context instance. provider (:class:`runway.cfngin.providers.base.BaseProvider`): The provider instance Keyword Args: function_arns (List[str]): The arns of all the Replicated functions to delete state_machine_arn (str): The ARN of the State Machine to execute stack_name (str): The name of the Cleanup stack to delete """ session = get_session(provider.region) step_functions_client = session.client('stepfunctions') try: step_functions_client.start_execution( stateMachineArn=kwargs['state_machine_arn'], input=json.dumps({ "SelfDestruct": { "StateMachineArn": kwargs['state_machine_arn'], "StackName": kwargs['stack_name'], }, "FunctionArns": kwargs['function_arns'] })) return True except Exception as err: # pylint: disable=broad-except LOGGER.error('Could not execute cleanup process.') LOGGER.error(err) return False
def delete( context, # type: Context # pylint: disable=unused-import provider, # type: BaseProvider **kwargs # type: Optional[Dict[str, Any]] ): # noqa: E124 # type: (...) -> Union[Dict[str, Any], bool] """Delete the domain if the user pool was created by Runway. If a User Pool was created by Runway, and populated with a domain, that domain must be deleted prior to the User Pool itself being deleted or an error will occur. This process ensures that our generated domain name is deleted, or skips if not able to find one. Args: context (:class:`runway.cfngin.context.Context`): The context instance. provider (:class:`runway.cfngin.providers.base.BaseProvider`): The provider instance Keyword Args: client_id (str): The ID of the Cognito User Pool Client """ session = get_session(provider.region) cognito_client = session.client('cognito-idp') user_pool_id = context.hook_data['aae_user_pool_id_retriever']['id'] client_id = kwargs['client_id'] (_, user_pool_hash) = user_pool_id.split('_') domain_prefix = ('%s-%s' % (user_pool_hash, client_id)).lower() try: cognito_client.delete_user_pool_domain(UserPoolId=user_pool_id, Domain=domain_prefix) return True except cognito_client.exceptions.InvalidParameterException: LOGGER.info('No domain found with prefix %s. Skipping deletion.', domain_prefix) return True except Exception as err: # pylint: disable=broad-except LOGGER.error('Could not delete the User Pool Domain.') LOGGER.error(err) return False
def test_ensure_cfn_bucket_does_not_exist_us_east(self) -> None: """Test ensure cfn bucket does not exist us east.""" session = get_session("us-east-1") provider = Provider(session) action = BaseAction( context=mock_context("mynamespace"), provider_builder=MockProviderBuilder(provider=provider), ) stubber = Stubber(action.s3_conn) stubber.add_client_error( "head_bucket", service_error_code="NoSuchBucket", service_message="Not Found", http_status_code=404, ) stubber.add_response("create_bucket", service_response={}, expected_params={"Bucket": ANY}) with stubber: action.ensure_cfn_bucket()
def setUp(self): """Run before tests.""" self.region = 'us-east-1' self.session = get_session(self.region) self.provider = Provider(self.session) self.config_no_persist = { 'stacks': [ {'name': 'stack1'}, {'name': 'stack2', 'requires': ['stack1']} ] } self.config_persist = { 'persistent_graph_key': 'test.json', 'stacks': [ {'name': 'stack1'}, {'name': 'stack2', 'requires': ['stack1']} ] }
def get( context, # pylint: disable=unused-argument provider, **kwargs): # noqa: E124 # type: (Context, BaseProvider, Optional[Dict[str, Any]]) -> Dict """Retrieve the callback URLs for User Pool Client Creation. When the User Pool is created a Callback URL is required. During a post hook entitled ``client_updater`` these Callback URLs are updated to that of the Distribution. Before then we need to ensure that if a Client already exists that the URLs for that client are used to prevent any interuption of service during deploy. Args: context (:class:`runway.cfngin.context.Context`): The context instance. provider (:class:`runway.cfngin.providers.base.BaseProvider`): The provider instance Keyword Args: user_pool_id (str): The ID of the User Pool to check for a client stack_name (str) The name of the stack to check against """ session = get_session(provider.region) cloudformation_client = session.client('cloudformation') cognito_client = session.client('cognito-idp') context_dict = {} context_dict['callback_urls'] = ['https://example.tmp'] try: # Return the current stack if one exists stack_desc = cloudformation_client.describe_stacks( StackName=kwargs['stack_name']) # Get the client_id from the outputs outputs = stack_desc['Stacks'][0]['Outputs'] if kwargs['user_pool_arn']: user_pool_id = kwargs['user_pool_arn'].split('/')[-1:][0] else: user_pool_id = [ o['OutputValue'] for o in outputs if o['OutputKey'] == 'AuthAtEdgeUserPoolId' ][0] client_id = [ o['OutputValue'] for o in outputs if o['OutputKey'] == 'AuthAtEdgeClient' ][0] # Poll the user pool client information resp = cognito_client.describe_user_pool_client( UserPoolId=user_pool_id, ClientId=client_id) # Retrieve the callbacks callbacks = resp['UserPoolClient']['CallbackURLs'] if callbacks: context_dict['callback_urls'] = callbacks return context_dict except Exception: # pylint: disable=broad-except return context_dict
def setUp(self) -> None: """Run before tests.""" region = "us-east-1" self.session = get_session(region=region) self.provider = Provider(self.session, region=region, recreate_failed=False) self.stubber = Stubber(self.provider.cloudformation)