PageRenderTime 56ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/src/sentry/integrations/bitbucket/integration.py

https://github.com/getsentry/sentry
Python | 214 lines | 211 code | 3 blank | 0 comment | 0 complexity | 37f4782872a51f40f413b8eb9476ccb5 MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause
  1. from django.utils.datastructures import OrderedSet
  2. from django.utils.translation import ugettext_lazy as _
  3. from sentry.identity.pipeline import IdentityProviderPipeline
  4. from sentry.integrations import (
  5. FeatureDescription,
  6. IntegrationFeatures,
  7. IntegrationInstallation,
  8. IntegrationMetadata,
  9. IntegrationProvider,
  10. )
  11. from sentry.integrations.atlassian_connect import (
  12. AtlassianConnectValidationError,
  13. get_integration_from_request,
  14. )
  15. from sentry.integrations.repositories import RepositoryMixin
  16. from sentry.models import Repository
  17. from sentry.pipeline import NestedPipelineView, PipelineView
  18. from sentry.shared_integrations.exceptions import ApiError
  19. from sentry.tasks.integrations import migrate_repo
  20. from sentry.utils.http import absolute_uri
  21. from .client import BitbucketApiClient
  22. from .issues import BitbucketIssueBasicMixin
  23. from .repository import BitbucketRepositoryProvider
  24. DESCRIPTION = """
  25. Connect your Sentry organization to Bitbucket, enabling the following features:
  26. """
  27. FEATURES = [
  28. FeatureDescription(
  29. """
  30. Track commits and releases (learn more
  31. [here](https://docs.sentry.io/learn/releases/))
  32. """,
  33. IntegrationFeatures.COMMITS,
  34. ),
  35. FeatureDescription(
  36. """
  37. Resolve Sentry issues via Bitbucket commits by
  38. including `Fixes PROJ-ID` in the message
  39. """,
  40. IntegrationFeatures.COMMITS,
  41. ),
  42. FeatureDescription(
  43. """
  44. Create Bitbucket issues from Sentry
  45. """,
  46. IntegrationFeatures.ISSUE_BASIC,
  47. ),
  48. FeatureDescription(
  49. """
  50. Link Sentry issues to existing Bitbucket issues
  51. """,
  52. IntegrationFeatures.ISSUE_BASIC,
  53. ),
  54. ]
  55. metadata = IntegrationMetadata(
  56. description=DESCRIPTION.strip(),
  57. features=FEATURES,
  58. author="The Sentry Team",
  59. noun=_("Installation"),
  60. issue_url="https://github.com/getsentry/sentry/issues/new?assignees=&labels=Component:%20Integrations&template=bug.yml&title=Bitbucket%20Integration%20Problem",
  61. source_url="https://github.com/getsentry/sentry/tree/master/src/sentry/integrations/bitbucket",
  62. aspects={},
  63. )
  64. # see https://developer.atlassian.com/bitbucket/api/2/reference/meta/authentication#scopes-bbc
  65. scopes = ("issue:write", "pullrequest", "webhook", "repository")
  66. class BitbucketIntegration(IntegrationInstallation, BitbucketIssueBasicMixin, RepositoryMixin):
  67. repo_search = True
  68. def get_client(self):
  69. return BitbucketApiClient(
  70. self.model.metadata["base_url"],
  71. self.model.metadata["shared_secret"],
  72. self.model.external_id,
  73. )
  74. @property
  75. def username(self):
  76. return self.model.name
  77. def error_message_from_json(self, data):
  78. return data.get("error", {}).get("message", "unknown error")
  79. def get_repositories(self, query=None):
  80. username = self.model.metadata.get("uuid", self.username)
  81. if not query:
  82. resp = self.get_client().get_repos(username)
  83. return [
  84. {"identifier": repo["full_name"], "name": repo["full_name"]}
  85. for repo in resp.get("values", [])
  86. ]
  87. exact_query = f'name="{query}"'.encode()
  88. fuzzy_query = f'name~"{query}"'.encode()
  89. exact_search_resp = self.get_client().search_repositories(username, exact_query)
  90. fuzzy_search_resp = self.get_client().search_repositories(username, fuzzy_query)
  91. result = OrderedSet()
  92. for j in exact_search_resp.get("values", []):
  93. result.add(j["full_name"])
  94. for i in fuzzy_search_resp.get("values", []):
  95. result.add(i["full_name"])
  96. return [{"identifier": full_name, "name": full_name} for full_name in result]
  97. def has_repo_access(self, repo):
  98. client = self.get_client()
  99. try:
  100. client.get_hooks(repo.config["name"])
  101. except ApiError:
  102. return False
  103. return True
  104. def get_unmigratable_repositories(self):
  105. repos = Repository.objects.filter(
  106. organization_id=self.organization_id, provider="bitbucket"
  107. )
  108. accessible_repos = [r["identifier"] for r in self.get_repositories()]
  109. return [repo for repo in repos if repo.name not in accessible_repos]
  110. def reinstall(self):
  111. self.reinstall_repositories()
  112. class BitbucketIntegrationProvider(IntegrationProvider):
  113. key = "bitbucket"
  114. name = "Bitbucket"
  115. metadata = metadata
  116. scopes = scopes
  117. integration_cls = BitbucketIntegration
  118. features = frozenset([IntegrationFeatures.ISSUE_BASIC, IntegrationFeatures.COMMITS])
  119. def get_pipeline_views(self):
  120. identity_pipeline_config = {"redirect_url": absolute_uri("/extensions/bitbucket/setup/")}
  121. identity_pipeline_view = NestedPipelineView(
  122. bind_key="identity",
  123. provider_key="bitbucket",
  124. pipeline_cls=IdentityProviderPipeline,
  125. config=identity_pipeline_config,
  126. )
  127. return [identity_pipeline_view, VerifyInstallation()]
  128. def post_install(self, integration, organization, extra=None):
  129. repo_ids = Repository.objects.filter(
  130. organization_id=organization.id,
  131. provider__in=["bitbucket", "integrations:bitbucket"],
  132. integration_id__isnull=True,
  133. ).values_list("id", flat=True)
  134. for repo_id in repo_ids:
  135. migrate_repo.apply_async(
  136. kwargs={
  137. "repo_id": repo_id,
  138. "integration_id": integration.id,
  139. "organization_id": organization.id,
  140. }
  141. )
  142. def build_integration(self, state):
  143. if state.get("publicKey"):
  144. principal_data = state["principal"]
  145. domain = principal_data["links"]["html"]["href"].replace("https://", "").rstrip("/")
  146. return {
  147. "provider": self.key,
  148. "external_id": state["clientKey"],
  149. "name": principal_data.get("username", principal_data["uuid"]),
  150. "metadata": {
  151. "public_key": state["publicKey"],
  152. "shared_secret": state["sharedSecret"],
  153. "base_url": state["baseApiUrl"],
  154. "domain_name": domain,
  155. "icon": principal_data["links"]["avatar"]["href"],
  156. "scopes": self.scopes,
  157. "uuid": principal_data["uuid"],
  158. "type": principal_data["type"], # team or user account
  159. },
  160. }
  161. else:
  162. return {
  163. "provider": self.key,
  164. "external_id": state["external_id"],
  165. "expect_exists": True,
  166. }
  167. def setup(self):
  168. from sentry.plugins.base import bindings
  169. bindings.add(
  170. "integration-repository.provider",
  171. BitbucketRepositoryProvider,
  172. id=f"integrations:{self.key}",
  173. )
  174. class VerifyInstallation(PipelineView):
  175. def dispatch(self, request, pipeline):
  176. try:
  177. integration = get_integration_from_request(request, BitbucketIntegrationProvider.key)
  178. except AtlassianConnectValidationError:
  179. return pipeline.error("Unable to verify installation.")
  180. pipeline.bind_state("external_id", integration.external_id)
  181. return pipeline.next_step()