def last_operation(self, instance_id: str, operation_data: Optional[str], **kwargs) -> LastOperation: """ Further readings `CF Broker API#LastOperation <https://docs.cloudfoundry.org/services/api.html#polling>`_ :param instance_id: Instance id provided by the platform :param operation_data: Operation data received from async operation :param kwargs: May contain additional information, improves compatibility with upstream versions :rtype: LastOperation """ instance = ServiceInstance.query.get(instance_id) if not instance: raise errors.ErrInstanceDoesNotExist if not operation_data: raise errors.ErrBadRequest(msg="Missing operation ID") operation = instance.operations.filter_by( id=int(operation_data)).first() if not operation: raise errors.ErrBadRequest( msg= f"Invalid operation id {operation_data} for service {instance_id}" ) return LastOperation( state=Operation.States(operation.state), description=operation.step_description, )
def validate(self): for header in self.header_list: if not header: raise errors.ErrBadRequest("Headers cannot be empty") header_chars = set(header) if not header_chars.issubset(HeaderList.ALLOWED_CHARACTERS): invalid_characters = header_chars.difference( HeaderList.ALLOWED_CHARACTERS ) raise errors.ErrBadRequest( f"{header} contains these invalid characters: {invalid_characters}" )
def provision_cdn_instance(instance_id: str, domain_names: list, params: dict): instance = CDNServiceInstance(id=instance_id, domain_names=domain_names) queue = queue_all_cdn_provision_tasks_for_operation instance.cloudfront_origin_hostname = params.get( "origin", config.DEFAULT_CLOUDFRONT_ORIGIN) validators.Hostname(instance.cloudfront_origin_hostname).validate() instance.cloudfront_origin_path = params.get("path", "") instance.route53_alias_hosted_zone = config.CLOUDFRONT_HOSTED_ZONE_ID forward_cookie_policy, forwarded_cookies = parse_cookie_options(params) instance.forward_cookie_policy = forward_cookie_policy instance.forwarded_cookies = forwarded_cookies forwarded_headers = parse_header_options(params) if instance.cloudfront_origin_hostname == config.DEFAULT_CLOUDFRONT_ORIGIN: forwarded_headers.append("HOST") forwarded_headers = normalize_header_list(forwarded_headers) instance.forwarded_headers = forwarded_headers instance.error_responses = params.get("error_responses", {}) validators.ErrorResponseConfig(instance.error_responses).validate() if params.get("insecure_origin", False): if params.get("origin") is None: raise errors.ErrBadRequest( "'insecure_origin' cannot be set when using the default origin." ) instance.origin_protocol_policy = CDNServiceInstance.ProtocolPolicy.HTTP.value else: instance.origin_protocol_policy = CDNServiceInstance.ProtocolPolicy.HTTPS.value return instance
def validate(self): if not isinstance(self.input, dict): raise errors.ErrBadRequest( "error_response should be a dictionary of error code: response path" ) for key, value in self.input.items(): if key not in ErrorResponseConfig.VALID_ERROR_CODES: raise errors.ErrBadRequest("error_response keys must be strings") if not isinstance(value, str): raise errors.ErrBadRequest("error_response values must be strings") if not value: raise errors.ErrBadRequest("error_response values must not be empty") if not value[0] == "/": raise errors.ErrBadRequest( "error_response path must be a path starting with `/`" )
def test_update_wraps_bad_request(self): self.broker.update.side_effect = errors.ErrBadRequest('BadRequest') response = self.client.patch( "/v2/service_instances/here-service-instance-id?accepts_incomplete=true", data=json.dumps({ "service_id": "service-guid-here", "plan_id": "plan-guid-here", "parameters": { "parameter1": 1 }, "previous_values": { "plan_id": "old-plan-guid-here", "service_id": "service-guid-here", "organization_id": "org-guid-here", "space_id": "space-guid-here" } }), headers={ 'X-Broker-Api-Version': '2.13', 'Content-Type': 'application/json', 'Authorization': self.auth_header }) self.assert400(response)
def provision(self, instance_id: str, details: ProvisionDetails, async_allowed: bool, **kwargs) -> ProvisionedServiceSpec: self.logger.info("starting provision request") if not async_allowed: raise errors.ErrAsyncRequired() params = details.parameters or {} domain_names = parse_domain_options(params) if not domain_names: raise errors.ErrBadRequest("'domains' parameter required.") self.logger.info("validating CNAMEs") validators.CNAME(domain_names).validate() self.logger.info("validating unique domains") if not config.IGNORE_DUPLICATE_DOMAINS: validators.UniqueDomains(domain_names).validate() if details.plan_id == CDN_PLAN_ID: instance = provision_cdn_instance(instance_id, domain_names, params) queue = queue_all_cdn_provision_tasks_for_operation elif details.plan_id == ALB_PLAN_ID: instance = ALBServiceInstance(id=instance_id, domain_names=domain_names) queue = queue_all_alb_provision_tasks_for_operation elif details.plan_id == MIGRATION_PLAN_ID: instance = MigrationServiceInstance(id=instance_id, domain_names=domain_names) db.session.add(instance) db.session.commit() return ProvisionedServiceSpec( state=ProvisionState.SUCCESSFUL_CREATED) else: raise NotImplementedError() self.logger.info("setting origin hostname") self.logger.info("creating operation") operation = Operation( state=Operation.States.IN_PROGRESS.value, service_instance=instance, action=Operation.Actions.PROVISION.value, step_description="Queuing tasks", ) db.session.add(instance) db.session.add(operation) self.logger.info("committing db session") db.session.commit() self.logger.info("queueing tasks") queue(operation.id, cf_logging.FRAMEWORK.context.get_correlation_id()) self.logger.info("all done. Returning provisioned service spec") return ProvisionedServiceSpec(state=ProvisionState.IS_ASYNC, operation=str(operation.id))
def test_deprovisioning_is_called_with_the_right_values(self): self.broker.deprovision.side_effect = errors.ErrBadRequest('BadRequest') response = self.client.delete( "/v2/service_instances/here_instance_id?service_id=service-guid-here&plan_id=plan-guid-here&accepts_incomplete=true", headers={ 'X-Broker-Api-Version': '2.13', 'Authorization': self.auth_header }) self.assert400(response)
def validate(self): instructions = self._instructions(self.domains) if instructions: msg = [ "An external domain service already exists for the following domains:" ] for error in instructions: msg.append(" " + error) raise errors.ErrBadRequest("\n".join(msg))
def test_unbind_is_called_with_the_right_values(self): self.broker.unbind.side_effect = errors.ErrBadRequest('BadRequest') query = "service_id=service-guid-here&plan_id=plan-guid-here" response = self.client.delete( "/v2/service_instances/here_instance_id/service_bindings/here_binding_id?%s" % query, headers={ 'X-Broker-Api-Version': '2.13', 'Authorization': self.auth_header }) self.assert400(response)
def validate(self): instructions = self._instructions(self.domains) if instructions: msg = [ "We could not find correct CNAME records for one or more of your domains.", "Please ensure the following DNS records are in place and try to provision", "this service again:", ] for error in instructions: msg.append(" " + error) raise errors.ErrBadRequest("\n".join(msg))
def deprovision(self, instance_id: str, details: DeprovisionDetails, async_allowed: bool, **kwargs) -> DeprovisionServiceSpec: instance = self.service_instances.get(instance_id) if instance is None: raise errors.ErrInstanceDoesNotExist() if instance.get('state') == self.CREATED: print(self.service_instances[instance_id]) context_instance = self.service_instances[instance_id] project_name = context_instance.get('project_name') api_url = context_instance.get( 'repo_hooks_url') #api endpoint to delete hook hook_id = context_instance.get('repo_hook_id') #hook id jenkins_utils.deprovision_job(project_name, api_url, hook_id) del self.service_instances[instance_id] return DeprovisionServiceSpec(False) elif instance.get('state') in [self.BOUND, self.BINDING]: raise errors.ErrBadRequest("Instance Binded,Can't deprovision")
def provision(self, instance_id: str, details: ProvisionDetails, async_allowed: bool, **kwargs) -> ProvisionedServiceSpec: self.logger.info("starting provision request") if not async_allowed: raise errors.ErrAsyncRequired() params = details.parameters or {} if params.get("domains"): domain_names = [ d.strip().lower() for d in params["domains"].split(",") ] else: raise errors.ErrBadRequest("'domains' parameter required.") self.logger.info("validating CNAMEs") validators.CNAME(domain_names).validate() self.logger.info("validating unique domains") validators.UniqueDomains(domain_names).validate() instance = ServiceInstance(id=instance_id, domain_names=domain_names) self.logger.info("setting origin hostname") instance.cloudfront_origin_hostname = params.get( "origin", config.DEFAULT_CLOUDFRONT_ORIGIN) instance.cloudfront_origin_path = params.get("path", "") self.logger.info("creating operation") operation = Operation( state=Operation.States.IN_PROGRESS.value, service_instance=instance, action=Operation.Actions.PROVISION.value, ) db.session.add(instance) db.session.add(operation) self.logger.info("committing db session") db.session.commit() self.logger.info("queueing tasks") queue_all_provision_tasks_for_operation( operation.id, cf_logging.FRAMEWORK.context.get_correlation_id()) self.logger.info("all done. Returning provisioned service spec") return ProvisionedServiceSpec(state=ProvisionState.IS_ASYNC, operation=str(operation.id))
def deprovision( self, instance_id: str, details: DeprovisionDetails, async_allowed: bool, **kwargs, ) -> DeprovisionServiceSpec: if not async_allowed: raise errors.ErrAsyncRequired() instance = ServiceInstance.query.get(instance_id) if not instance: raise errors.ErrInstanceDoesNotExist operation = Operation( state=Operation.States.IN_PROGRESS.value, service_instance=instance, action=Operation.Actions.DEPROVISION.value, step_description="Queuing tasks", ) db.session.add(operation) db.session.commit() if details.plan_id == CDN_PLAN_ID: queue_all_cdn_deprovision_tasks_for_operation( operation.id, cf_logging.FRAMEWORK.context.get_correlation_id()) elif details.plan_id == ALB_PLAN_ID: queue_all_alb_deprovision_tasks_for_operation( operation.id, cf_logging.FRAMEWORK.context.get_correlation_id()) elif details.plan_id == MIGRATION_PLAN_ID: for o in instance.operations: if o.action == Operation.Actions.UPDATE.value: raise errors.ErrBadRequest( msg="Can't delete migration with update operations") queue_all_migration_deprovision_tasks_for_operation( operation.id, cf_logging.FRAMEWORK.context.get_correlation_id()) else: raise NotImplementedError() return DeprovisionServiceSpec(is_async=True, operation=str(operation.id))
def test_bind_called_with_the_right_values(self): self.broker.bind.side_effect = errors.ErrBadRequest('BadRequest') response = self.client.put( "/v2/service_instances/here-instance_id/service_bindings/here-binding_id", data=json.dumps({ "service_id": "service-guid-here", "plan_id": "plan-guid-here", "bind_resource": { "app_guid": "app-guid-here", "route": "route-here" }, "parameters": { "parameter1": 1 } }), headers={ 'X-Broker-Api-Version': '2.13', 'Content-Type': 'application/json', 'Authorization': self.auth_header }) self.assert400(response)
def update(self, instance_id: str, details: UpdateDetails, async_allowed: bool, **kwargs) -> UpdateServiceSpec: if not async_allowed: raise errors.ErrAsyncRequired() params = details.parameters or {} instance = ServiceInstance.query.get(instance_id) if not instance: raise errors.ErrBadRequest("Service instance does not exist") if instance.deactivated_at: raise errors.ErrBadRequest( "Cannot update instance because it was already canceled") if instance.has_active_operations(): raise errors.ErrBadRequest( "Instance has an active operation in progress") domain_names = parse_domain_options(params) noop = True if domain_names is not None: self.logger.info("validating CNAMEs") validators.CNAME(domain_names).validate() self.logger.info("validating unique domains") validators.UniqueDomains(domain_names).validate(instance) noop = noop and (sorted(domain_names) == sorted( instance.domain_names)) if instance.instance_type == "cdn_service_instance" and noop: instance.new_certificate = instance.current_certificate instance.domain_names = domain_names if instance.instance_type == "cdn_service_instance": # N.B. we're using "param" in params rather than # params.get("param") because the OSBAPI spec # requires we do not mess with params that were not # specified, so unset and set to None have different meanings noop = False if details.plan_id != CDN_PLAN_ID: raise ClientError("Updating service plan is not supported") if "origin" in params: if params["origin"]: origin_hostname = params["origin"] validators.Hostname(origin_hostname).validate() else: origin_hostname = config.DEFAULT_CLOUDFRONT_ORIGIN instance.cloudfront_origin_hostname = origin_hostname if "path" in params: if params["path"]: cloudfront_origin_path = params["path"] else: cloudfront_origin_path = "" instance.cloudfront_origin_path = cloudfront_origin_path if "forward_cookies" in params: forward_cookie_policy, forwarded_cookies = parse_cookie_options( params) instance.forward_cookie_policy = forward_cookie_policy instance.forwarded_cookies = forwarded_cookies if "forward_headers" in params: forwarded_headers = parse_header_options(params) else: # .copy() so sqlalchemy recognizes the field has changed forwarded_headers = instance.forwarded_headers.copy() if instance.cloudfront_origin_hostname == config.DEFAULT_CLOUDFRONT_ORIGIN: forwarded_headers.append("HOST") forwarded_headers = normalize_header_list(forwarded_headers) instance.forwarded_headers = forwarded_headers if "insecure_origin" in params: origin_protocol_policy = "https-only" if params["insecure_origin"]: if (instance.cloudfront_origin_hostname == config.DEFAULT_CLOUDFRONT_ORIGIN): raise errors.ErrBadRequest( "Cannot use insecure_origin with default origin") origin_protocol_policy = "http-only" instance.origin_protocol_policy = origin_protocol_policy if "error_responses" in params: instance.error_responses = params["error_responses"] validators.ErrorResponseConfig( instance.error_responses).validate() queue = queue_all_cdn_update_tasks_for_operation elif instance.instance_type == "alb_service_instance": if details.plan_id != ALB_PLAN_ID: raise ClientError("Updating service plan is not supported") queue = queue_all_alb_update_tasks_for_operation elif instance.instance_type == "migration_service_instance": if details.plan_id == CDN_PLAN_ID: noop = False validate_migration_to_cdn_params(params) instance = change_instance_type(instance, CDNServiceInstance, db.session) update_cdn_params_for_migration(instance, params) db.session.add(instance.current_certificate) queue = queue_all_cdn_broker_migration_tasks_for_operation elif details.plan_id == ALB_PLAN_ID: noop = False validate_migration_to_alb_params(params) instance = change_instance_type(instance, ALBServiceInstance, db.session) update_alb_params_for_migration(instance, params) db.session.add(instance.current_certificate) queue = queue_all_domain_broker_migration_tasks_for_operation else: raise ClientError( "Updating to this service plan is not supported") if noop: return UpdateServiceSpec(False) operation = Operation( state=Operation.States.IN_PROGRESS.value, service_instance=instance, action=Operation.Actions.UPDATE.value, step_description="Queuing tasks", ) db.session.add(operation) db.session.add(instance) db.session.commit() queue(operation.id, cf_logging.FRAMEWORK.context.get_correlation_id()) return UpdateServiceSpec(True, operation=str(operation.id))
def validate(self): if not re.fullmatch(Hostname.domain_name, self.origin): raise errors.ErrBadRequest(f"{self.origin} is not a valid hostname") if len(self.origin) > 253: raise errors.ErrBadRequest(f"{self.origin} is not a valid hostname")