client.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import calendar
  2. import datetime
  3. import time
  4. from sentry import options
  5. from sentry.services.hybrid_cloud.integration.model import RpcIntegration
  6. from sentry.utils import jwt
  7. from sentry_plugins.client import ApiClient, AuthApiClient
  8. class GithubPluginClientMixin(AuthApiClient):
  9. allow_redirects = True
  10. base_url = "https://api.github.com"
  11. plugin_name = "github"
  12. def get_last_commits(self, repo, end_sha):
  13. # return api request that fetches last ~30 commits
  14. # see https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository
  15. # using end_sha as parameter
  16. return self.get(f"/repos/{repo}/commits", params={"sha": end_sha})
  17. def compare_commits(self, repo, start_sha, end_sha):
  18. # see https://developer.github.com/v3/repos/commits/#compare-two-commits
  19. # where start sha is oldest and end is most recent
  20. return self.get(f"/repos/{repo}/compare/{start_sha}...{end_sha}")
  21. def get_pr_commits(self, repo, num):
  22. # see https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request
  23. # Max: 250 Commits
  24. return self.get(f"/repos/{repo}/pulls/{num}/commits")
  25. class GithubPluginClient(GithubPluginClientMixin, AuthApiClient):
  26. def __init__(self, url=None, auth=None):
  27. if url is not None:
  28. self.base_url = url.rstrip("/")
  29. super().__init__(auth=auth)
  30. def request_no_auth(self, method, path, data=None, params=None):
  31. if params is None:
  32. params = {}
  33. return self._request(method, path, auth=None, data=data, params=params)
  34. def get_repo(self, repo):
  35. return self.get(f"/repos/{repo}")
  36. def get_issue(self, repo, issue_id):
  37. return self.get(f"/repos/{repo}/issues/{issue_id}")
  38. def create_issue(self, repo, data):
  39. return self.post(f"/repos/{repo}/issues", data=data)
  40. def create_comment(self, repo, issue_id, data):
  41. return self.post(f"/repos/{repo}/issues/{issue_id}/comments", data=data)
  42. def list_assignees(self, repo):
  43. return self.get(f"/repos/{repo}/assignees?per_page=100")
  44. def search_issues(self, query):
  45. return self.get("/search/issues", params={"q": query})
  46. def create_hook(self, repo, data):
  47. return self.post(f"/repos/{repo}/hooks", data=data)
  48. def update_hook(self, repo, hook_id, data):
  49. return self.patch(f"/repos/{repo}/hooks/{hook_id}", data=data)
  50. def delete_hook(self, repo, id):
  51. return self.delete(f"/repos/{repo}/hooks/{id}")
  52. def get_installations(self):
  53. # TODO(jess): remove this whenever it's out of preview
  54. headers = {"Accept": "application/vnd.github.machine-man-preview+json"}
  55. return self._request("GET", "/user/installations", headers=headers)
  56. class GithubPluginAppsClient(GithubPluginClientMixin, ApiClient):
  57. def __init__(self, integration: RpcIntegration):
  58. self.integration = integration
  59. self.token: str | None = None
  60. self.expires_at: datetime.datetime | None = None
  61. super().__init__()
  62. def get_token(self):
  63. if not self.token or (
  64. self.expires_at is not None and self.expires_at < datetime.datetime.utcnow()
  65. ):
  66. res = self.create_token()
  67. self.token = res["token"]
  68. self.expires_at = datetime.datetime.strptime(res["expires_at"], "%Y-%m-%dT%H:%M:%SZ")
  69. return self.token
  70. def get_jwt(self) -> str:
  71. exp_dt = datetime.datetime.utcnow() + datetime.timedelta(minutes=10)
  72. exp = calendar.timegm(exp_dt.timetuple())
  73. # Generate the JWT
  74. payload = {
  75. # issued at time
  76. "iat": int(time.time()),
  77. # JWT expiration time (10 minute maximum)
  78. "exp": exp,
  79. # Integration's GitHub identifier
  80. "iss": options.get("github.integration-app-id"),
  81. }
  82. return jwt.encode(payload, options.get("github.integration-private-key"), algorithm="RS256")
  83. def request(self, method, path, headers=None, data=None, params=None):
  84. if headers is None:
  85. headers = {
  86. "Authorization": "token %s" % self.get_token(),
  87. # TODO(jess): remove this whenever it's out of preview
  88. "Accept": "application/vnd.github.machine-man-preview+json",
  89. }
  90. return self._request(method, path, headers=headers, data=data, params=params)
  91. def create_token(self):
  92. headers = {
  93. # TODO(jess): remove this whenever it's out of preview
  94. "Accept": "application/vnd.github.machine-man-preview+json",
  95. }
  96. headers.update(jwt.authorization_header(self.get_jwt()))
  97. return self.post(
  98. f"/app/installations/{self.integration.external_id}/access_tokens",
  99. headers=headers,
  100. )
  101. def get_repositories(self):
  102. return self.get("/installation/repositories")