def get_repository_choices(self, group, **kwargs): """ Returns the default repository and a set/subset of repositories of associated with the installation """ try: repos = self.get_repositories() except ApiError: raise IntegrationError( "Unable to retrieve repositories. Please try again later.") else: repo_choices = [(repo["identifier"], repo["name"]) for repo in repos] repo = kwargs.get("repo") if not repo: params = kwargs.get("params", {}) defaults = self.get_project_defaults(group.project_id) repo = params.get("repo", defaults.get("repo")) try: default_repo = repo or repo_choices[0][0] except IndexError: return "", repo_choices # If a repo has been selected outside of the default list of # repos, stick it onto the front of the list so that it can be # selected. try: next(True for r in repo_choices if r[0] == default_repo) except StopIteration: repo_choices.insert(0, self.create_default_repo_choice(default_repo)) return default_repo, repo_choices
def dispatch(self, request, pipeline): data = pipeline.fetch_state("msteams") # check the expiration time of the link if int(time.time()) > data["expiration_time"]: return pipeline.error( IntegrationError("Installation link expired")) return pipeline.next_step()
def update_organization_config(self, data: MutableMapping[str, Any]) -> None: if "sync_status_forward" in data: project_ids_and_statuses = data.pop("sync_status_forward") if any(not mapping["on_unresolve"] or not mapping["on_resolve"] for mapping in project_ids_and_statuses.values()): raise IntegrationError( "Resolve and unresolve status are required.") data["sync_status_forward"] = bool(project_ids_and_statuses) IntegrationExternalProject.objects.filter( organization_integration_id=self.org_integration.id).delete() for project_id, statuses in project_ids_and_statuses.items(): IntegrationExternalProject.objects.create( organization_integration_id=self.org_integration.id, external_id=project_id, resolved_status=statuses["on_resolve"], unresolved_status=statuses["on_unresolve"], ) config = self.org_integration.config config.update(data) self.org_integration.update(config=config)
def _get_channels_from_rules(pipeline): organization = pipeline.organization integration_id = pipeline.fetch_state("integration_id") try: integration = Integration.objects.get( id=integration_id, provider="slack", ) except Integration.DoesNotExist: raise IntegrationError("Could not find Slack integration.") rules = Rule.objects.filter( project__in=organization.project_set.all(), status=RuleStatus.ACTIVE, ) channels = set() for rule in rules: # try and see if its used for slack for rule_action in rule.data["actions"]: rule_integration_id = rule_action.get("workspace") if rule_integration_id and rule_integration_id == six.text_type( integration.id): channel_id = rule_action["channel_id"] channel_name = rule_action["channel"] # don't care if its a user if channel_name[0] == "@": continue channels.add(Channel(channel_name, channel_id)) return channels
def get_option_value(function, option): region = parse_arn(function["FunctionArn"])["region"] runtime = function["Runtime"] # currently only supporting node runtimes if runtime.startswith("nodejs"): prefix = "node" elif runtime.startswith("python"): prefix = "python" else: raise Exception("Unsupported runtime") # account number doesn't depend on the runtime prefix if option == OPTION_ACCOUNT_NUMBER: option_field = "aws-lambda.account-number" else: option_field = f"aws-lambda.{prefix}.{option}" # if we don't have the settings set, read from our options if not settings.SENTRY_RELEASE_REGISTRY_BASEURL: return options.get(option_field) # otherwise, read from the cache cache_options = cache.get(LAYER_INDEX_CACHE_KEY) or {} key = f"aws-layer:{prefix}" cache_value = cache_options.get(key) if cache_value is None: raise IntegrationError(f"Could not find cache value with key {key}") # special lookup for the version since it depends on the region if option == OPTION_VERSION: region_release_list = cache_value.get("regions", []) matched_regions = filter(lambda x: x["region"] == region, region_release_list) # see if there is the specific region in our list if matched_regions: version = matched_regions[0]["version"] return version else: raise IntegrationError(f"Unsupported region {region}") # we use - in options but _ in the registry registry_field = option.replace("-", "_") # return the value out of the cache return cache_value.get(registry_field)
def create_issue(self, data, **kwargs): if "assignee" not in data: raise IntegrationError("Assignee is required") return { "key": "APP-123", "title": "This is a test external issue title", "description": "This is a test external issue description", }
def after_link_issue(self, external_issue: ExternalIssue, **kwargs: Any) -> None: data = kwargs["data"] client = self.get_client() repo, issue_num = external_issue.key.split("#") if not repo: raise IntegrationError("repo must be provided") if not issue_num: raise IntegrationError("issue number must be provided") comment = data.get("comment") if comment: try: client.create_comment(repo=repo, issue_id=issue_num, data={"body": comment}) except ApiError as e: raise IntegrationError(self.message_from_error(e))
def raise_error(self, exc: ApiError, identity: Optional[Identity] = None) -> None: if isinstance(exc, ApiUnauthorized): raise InvalidIdentity(self.message_from_error(exc), identity=identity).with_traceback( sys.exc_info()[2] ) elif isinstance(exc, ApiError): if exc.json: error_fields = self.error_fields_from_json(exc.json) if error_fields is not None: raise IntegrationFormError(error_fields).with_traceback(sys.exc_info()[2]) raise IntegrationError(self.message_from_error(exc)).with_traceback(sys.exc_info()[2]) elif isinstance(exc, IntegrationError): raise else: self.logger.exception(str(exc)) raise IntegrationError(self.message_from_error(exc)).with_traceback(sys.exc_info()[2])
def after_link_issue(self, external_issue, **kwargs): data = kwargs["data"] project_id, issue_id = data.get("externalIssue", "").split("#") if not (project_id and issue_id): raise IntegrationError("Project and Issue id must be provided") client = self.get_client() comment = data.get("comment") if not comment: return try: client.create_issue_comment( project_id=project_id, issue_id=issue_id, data={"body": comment} ) except ApiError as e: raise IntegrationError(self.message_from_error(e))
def get_installation(self, integration_id, organization_id): if integration_id is None: raise IntegrationError("Bitbucket Server requires an integration id.") integration_model = Integration.objects.get( id=integration_id, organizations=organization_id, provider="bitbucket_server" ) return integration_model.get_installation(organization_id)
def compare_commits(self, repo, start_sha, end_sha): installation = Integration.objects.get( id=repo.integration_id).get_installation(repo.organization_id) try: raise IntegrationError("{'error': 'Repository not found'}") except Exception as e: installation.raise_error(e)
def get_installation(self, integration_id, organization_id): if integration_id is None: raise IntegrationError("%s requires an integration id." % self.name) integration_model = Integration.objects.get( id=integration_id, organizations=organization_id, provider="vsts") return integration_model.get_installation(organization_id)
def wrapped(*args, **kwargs): try: return func(*args, **kwargs) except Exception as e: err_message = str(e) is_custom_err, err_message = get_sentry_err_message(err_message) if is_custom_err: raise IntegrationError(_(err_message)) # otherwise, re-raise the original error raise
def dispatch(self, request, pipeline): integration_id = request.GET.get("integration_id") if integration_id: pipeline.bind_state("integration_id", integration_id) pipeline.bind_state("user_id", request.user.id) try: integration = Integration.objects.get( id=integration_id, provider="slack", ) except Integration.DoesNotExist: return pipeline.error( IntegrationError("Could not find Slack integration.")) # We check if there are any other orgs tied to the integration to let the # user know those organizations will be affected by the migration extra_orgs = map( lambda x: x.slug, integration.organizations.filter(~Q( id=pipeline.organization.id))) pipeline.bind_state("extra_orgs", extra_orgs) try: all_channels = _get_channels_from_rules( pipeline.organization, integration) except IntegrationError as error: return pipeline.error(error) pipeline.bind_state("all_channels", all_channels) next_param = "?show_verification_results" return render_to_response( template="sentry/integrations/slack-reauth-introduction.html", context={ "next_url": "%s%s" % (absolute_uri("/extensions/slack/setup/"), next_param), "workspace": integration.name, "extra_orgs": extra_orgs, }, request=request, ) if "show_verification_results" in request.GET: return pipeline.next_step() # if we dont have the integration_id we dont care about the # migration path, skip straight to install return pipeline.next_step(step_size=2)
def wrapped(*args, **kwargs): try: return func(*args, **kwargs) except Exception as e: err_message = six.text_type(e) invalid_layer = get_invalid_layer_name(err_message) # only have one specific error to catch if invalid_layer: raise IntegrationError(_(INVALID_LAYER_TEXT) % invalid_layer) # otherwise, re-raise the original error raise
def get_team_info(self, access_token): payload = {"token": access_token} client = SlackClient() try: resp = client.get("/team.info", params=payload) except ApiError as e: logger.error("slack.team-info.response-error", extra={"error": six.text_type(e)}) raise IntegrationError("Could not retrieve Slack team information.") return resp["team"]
def update_env_variable(self, client, vercel_project_id, data): envs = client.get_env_vars(vercel_project_id)["envs"] env_var_ids = [env_var["id"] for env_var in envs if env_var["key"] == data["key"]] if env_var_ids: return client.update_env_variable(vercel_project_id, env_var_ids[0], data) key = data["key"] raise IntegrationError( f"Could not update environment variable {key} in Vercel project {vercel_project_id}." )
def get_client(self): if self.default_identity is None: try: self.default_identity = self.get_default_identity() except Identity.DoesNotExist: raise IntegrationError("Identity not found.") return JiraServerClient( self.model.metadata["base_url"], JiraServer(self.default_identity.data), self.model.metadata["verify_ssl"], )
def build_integration(self, state): data = state["identity"]["data"] access_token = data["access_token"] team_id = data.get("team_id") client = VercelClient(access_token, team_id) if team_id: external_id = team_id installation_type = "team" team = client.get_team() name = team["name"] else: external_id = data["user_id"] installation_type = "user" user = client.get_user() name = user["name"] try: webhook = client.create_deploy_webhook() except ApiError as err: logger.info( "vercel.create_webhook.failed", extra={ "error": six.text_type(err), "external_id": external_id }, ) try: details = err.json["messages"][0].values().pop() except Exception: details = "Unknown Error" message = u"Could not create deployment webhook in Vercel: {}".format( details) raise IntegrationError(message) configurations = self.get_configuration_metadata(external_id) integration = { "name": name, "external_id": external_id, "metadata": { "access_token": access_token, "installation_id": data["installation_id"], "installation_type": installation_type, "webhook_id": webhook["id"], "configurations": configurations, }, "post_install_data": { "user_id": state["user_id"] }, } return integration
def get_payload_and_token(self, payload, organization_id, sentry_project_id): meta = payload["deployment"]["meta"] # look up the project so we can get the slug project = Project.objects.get(id=sentry_project_id) # find the connected sentry app installation installation_for_provider = SentryAppInstallationForProvider.objects.select_related( "sentry_app_installation").get(organization_id=organization_id, provider=self.provider) sentry_app_installation = installation_for_provider.sentry_app_installation # find a token associated with the installation so we can use it for authentication sentry_app_installation_token = ( SentryAppInstallationToken.objects.select_related("api_token"). filter(sentry_app_installation=sentry_app_installation).first()) if not sentry_app_installation_token: raise SentryAppInstallationToken.DoesNotExist() # find the commmit sha so we can use it as as the release commit_sha = (meta.get("githubCommitSha") or meta.get("gitlabCommitSha") or meta.get("bitbucketCommitSha")) # contruct the repo depeding what provider we use if meta.get("githubCommitSha"): # we use these instead of githubOrg and githubRepo since it's the repo the user has access to repository = u"%s/%s" % (meta["githubCommitOrg"], meta["githubCommitRepo"]) elif meta.get("gitlabCommitSha"): # gitlab repos are formatted with a space for some reason repository = u"%s / %s" % ( meta["gitlabProjectNamespace"], meta["gitlabProjectName"], ) elif meta.get("bitbucketCommitSha"): repository = u"%s/%s" % (meta["bitbucketRepoOwner"], meta["bitbucketRepoName"]) else: # this should really never happen raise IntegrationError("No commit found") release_payload = { "version": commit_sha, "projects": [project.slug], "refs": [{ "repository": repository, "commit": commit_sha }], } return [release_payload, sentry_app_installation_token.api_token.token]
def get_team_info(self, access_token: str) -> JSONData: headers = {"Authorization": f"Bearer {access_token}"} client = SlackClient() try: resp = client.get("/team.info", headers=headers) except ApiError as e: logger.error("slack.team-info.response-error", extra={"error": str(e)}) raise IntegrationError( "Could not retrieve Slack team information.") return resp["team"]
def get_installation( self, integration_id: Optional[int], organization_id: int ) -> IntegrationInstallation: if integration_id is None: raise IntegrationError(f"{self.name} requires an integration id.") integration_model = Integration.objects.get( id=integration_id, organizations=organization_id, provider="vsts" ) # Explicitly typing to satisfy mypy. installation: IntegrationInstallation = integration_model.get_installation(organization_id) return installation
def _validate_repo(self, client, installation, repo): try: repo_data = client.get_repo(repo) except Exception as e: installation.raise_error(e) try: # make sure installation has access to this specific repo client.get_commits(repo) except ApiError: raise IntegrationError(f"You must grant Sentry access to {repo}") return repo_data
def get_repositories(self, query=None): try: repos = self.get_client().get_repos(self.instance) except (ApiError, IdentityNotValid) as e: raise IntegrationError(self.message_from_error(e)) data = [] for repo in repos["value"]: data.append( { "name": "%s/%s" % (repo["project"]["name"], repo["name"]), "identifier": repo["id"], } ) return data
def sync_status_outbound(self, external_issue, is_resolved, project_id, **kwargs): """ Propagate a sentry issue's status to a linked issue's status. """ client = self.get_client() jira_issue = client.get_issue(external_issue.key) jira_project = jira_issue["fields"]["project"] try: external_project = IntegrationExternalProject.objects.get( external_id=jira_project["id"], organization_integration_id__in=OrganizationIntegration. objects.filter( organization_id=external_issue.organization_id, integration_id=external_issue.integration_id, ), ) except IntegrationExternalProject.DoesNotExist: return jira_status = (external_project.resolved_status if is_resolved else external_project.unresolved_status) # don't bother updating if it's already the status we'd change it to if jira_issue["fields"]["status"]["id"] == jira_status: return try: transitions = client.get_transitions(external_issue.key) except ApiHostError: raise IntegrationError("Could not reach host to get transitions.") try: transition = [ t for t in transitions if t.get("to", {}).get("id") == jira_status ][0] except IndexError: # TODO(jess): Email for failure logger.warning( "jira.status-sync-fail", extra={ "organization_id": external_issue.organization_id, "integration_id": external_issue.integration_id, "issue_key": external_issue.key, }, ) return client.transition_issue(external_issue.key, transition["id"])
def validate_channel_id(name: str, integration_id: int, input_channel_id: str) -> None: """ In the case that the user is creating an alert via the API and providing the channel ID and name themselves, we want to make sure both values are correct. """ try: integration = Integration.objects.get(id=integration_id) except Integration.DoesNotExist: raise Http404 token = integration.metadata["access_token"] headers = {"Authorization": f"Bearer {token}"} payload = {"channel": input_channel_id} client = SlackClient() try: results = client.get("/conversations.info", headers=headers, params=payload) except ApiError as e: if e.text == "channel_not_found": raise ValidationError("Channel not found. Invalid ID provided.") logger.info("rule.slack.conversation_info_failed", extra={"error": str(e)}) raise IntegrationError("Could not retrieve Slack channel information.") if not isinstance(results, dict): raise IntegrationError("Bad slack channel list response.") stripped_channel_name = strip_channel_name(name) if not stripped_channel_name == results["channel"]["name"]: channel_name = results["channel"]["name"] raise ValidationError( f"Received channel name {channel_name} does not match inputted channel name {stripped_channel_name}." )
def get_issue(self, issue_id, **kwargs): data = kwargs["data"] repo = data.get("repo") issue_num = data.get("externalIssue") client = self.get_client() if not repo: raise IntegrationError("repo must be provided") if not issue_num: raise IntegrationError("issue must be provided") try: issue = client.get_issue(repo, issue_num) except ApiError as e: raise IntegrationError(self.message_from_error(e)) return { "key": issue["number"], "title": issue["title"], "description": issue["body"], "url": issue["html_url"], "repo": repo, }
def _validate_repo(self, client, installation, repo): try: repo_data = client.get_repo(repo) except Exception as e: installation.raise_error(e) try: # make sure installation has access to this specific repo # use hooks endpoint since we explicitly ask for those permissions # when installing the app (commits can be accessed for public repos) # https://developer.github.com/v3/repos/hooks/#list-hooks client.repo_hooks(repo) except ApiError: raise IntegrationError("You must grant Sentry access to {}".format(repo)) return repo_data
def test_get_create_with_error(self): self.login_as(user=self.user) org = self.organization integration = Integration.objects.create(provider="example", name="Example") integration.add_organization(org, self.user) path = f"/api/0/issues/{self.group.id}/integrations/{integration.id}/?action=create" with self.feature("organizations:integrations-issue-basic"): with mock.patch.object( ExampleIntegration, "get_create_issue_config", side_effect=IntegrationError("oops") ): response = self.client.get(path) assert response.status_code == 400 assert response.data == {"detail": "oops"}
def get_repositories(self, query: Optional[str] = None ) -> Sequence[Mapping[str, str]]: try: repos = self.get_client().get_repos(self.instance) except (ApiError, IdentityNotValid) as e: raise IntegrationError(self.message_from_error(e)) data = [] for repo in repos["value"]: data.append({ "name": "{}/{}".format(repo["project"]["name"], repo["name"]), "identifier": repo["id"], }) return data