PageRenderTime 54ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

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

https://github.com/pombredanne/django-sentry
Python | 205 lines | 201 code | 4 blank | 0 comment | 0 complexity | 613c822676bd1bb1684aa99ac185b7d8 MD5 | raw file
  1. from __future__ import absolute_import
  2. from sentry.integrations import (
  3. IntegrationInstallation,
  4. IntegrationFeatures,
  5. IntegrationProvider,
  6. IntegrationMetadata,
  7. FeatureDescription,
  8. )
  9. from sentry.integrations.atlassian_connect import (
  10. AtlassianConnectValidationError,
  11. get_integration_from_request,
  12. )
  13. from sentry.integrations.repositories import RepositoryMixin
  14. from sentry.pipeline import NestedPipelineView, PipelineView
  15. from sentry.identity.pipeline import IdentityProviderPipeline
  16. from django.utils.translation import ugettext_lazy as _
  17. from sentry.integrations.exceptions import ApiError
  18. from sentry.models import Repository
  19. from sentry.tasks.integrations import migrate_repo
  20. from sentry.utils.http import absolute_uri
  21. from .repository import BitbucketRepositoryProvider
  22. from .client import BitbucketApiClient
  23. from .issues import BitbucketIssueBasicMixin
  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?title=Bitbucket%20Integration:%20&labels=Component%3A%20Integrations",
  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")
  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. if not query:
  81. resp = self.get_client().get_repos(self.username)
  82. return [
  83. {"identifier": repo["full_name"], "name": repo["full_name"]}
  84. for repo in resp.get("values", [])
  85. ]
  86. full_query = (u'name~"%s"' % (query)).encode("utf-8")
  87. resp = self.get_client().search_repositories(self.username, full_query)
  88. return [
  89. {"identifier": i["full_name"], "name": i["full_name"]} for i in resp.get("values", [])
  90. ]
  91. def has_repo_access(self, repo):
  92. client = self.get_client()
  93. try:
  94. client.get_hooks(repo.config["name"])
  95. except ApiError:
  96. return False
  97. return True
  98. def get_unmigratable_repositories(self):
  99. repos = Repository.objects.filter(
  100. organization_id=self.organization_id, provider="bitbucket"
  101. )
  102. accessible_repos = [r["identifier"] for r in self.get_repositories()]
  103. return filter(lambda repo: repo.name not in accessible_repos, repos)
  104. def reinstall(self):
  105. self.reinstall_repositories()
  106. class BitbucketIntegrationProvider(IntegrationProvider):
  107. key = "bitbucket"
  108. name = "Bitbucket"
  109. metadata = metadata
  110. scopes = scopes
  111. integration_cls = BitbucketIntegration
  112. features = frozenset([IntegrationFeatures.ISSUE_BASIC, IntegrationFeatures.COMMITS])
  113. def get_pipeline_views(self):
  114. identity_pipeline_config = {"redirect_url": absolute_uri("/extensions/bitbucket/setup/")}
  115. identity_pipeline_view = NestedPipelineView(
  116. bind_key="identity",
  117. provider_key="bitbucket",
  118. pipeline_cls=IdentityProviderPipeline,
  119. config=identity_pipeline_config,
  120. )
  121. return [identity_pipeline_view, VerifyInstallation()]
  122. def post_install(self, integration, organization):
  123. repo_ids = Repository.objects.filter(
  124. organization_id=organization.id,
  125. provider__in=["bitbucket", "integrations:bitbucket"],
  126. integration_id__isnull=True,
  127. ).values_list("id", flat=True)
  128. for repo_id in repo_ids:
  129. migrate_repo.apply_async(
  130. kwargs={
  131. "repo_id": repo_id,
  132. "integration_id": integration.id,
  133. "organization_id": organization.id,
  134. }
  135. )
  136. def build_integration(self, state):
  137. if state.get("publicKey"):
  138. principal_data = state["principal"]
  139. domain = principal_data["links"]["html"]["href"].replace("https://", "").rstrip("/")
  140. return {
  141. "provider": self.key,
  142. "external_id": state["clientKey"],
  143. "name": principal_data.get("username", principal_data["uuid"]),
  144. "metadata": {
  145. "public_key": state["publicKey"],
  146. "shared_secret": state["sharedSecret"],
  147. "base_url": state["baseApiUrl"],
  148. "domain_name": domain,
  149. "icon": principal_data["links"]["avatar"]["href"],
  150. "scopes": self.scopes,
  151. "uuid": principal_data["uuid"],
  152. "type": principal_data["type"], # team or user account
  153. },
  154. }
  155. else:
  156. return {
  157. "provider": self.key,
  158. "external_id": state["external_id"],
  159. "expect_exists": True,
  160. }
  161. def setup(self):
  162. from sentry.plugins.base import bindings
  163. bindings.add(
  164. "integration-repository.provider",
  165. BitbucketRepositoryProvider,
  166. id="integrations:%s" % self.key,
  167. )
  168. class VerifyInstallation(PipelineView):
  169. def dispatch(self, request, pipeline):
  170. try:
  171. integration = get_integration_from_request(request, BitbucketIntegrationProvider.key)
  172. except AtlassianConnectValidationError:
  173. return pipeline.error("Unable to verify installation.")
  174. pipeline.bind_state("external_id", integration.external_id)
  175. return pipeline.next_step()