api.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. from django.http import Http404, HttpResponse
  2. from django.shortcuts import aget_object_or_404
  3. from ninja import Router
  4. from ninja.errors import ValidationError
  5. from ninja.pagination import paginate
  6. from apps.files.tasks import assemble_artifacts_task
  7. from apps.organizations_ext.models import Organization
  8. from apps.projects.models import Project
  9. from apps.sourcecode.models import DebugSymbolBundle
  10. from apps.sourcecode.schema import DebugSymbolBundleSchema
  11. from glitchtip.api.authentication import AuthHttpRequest
  12. from glitchtip.api.permissions import has_permission
  13. from glitchtip.utils import async_call_celery_task
  14. from .models import Release
  15. from .schema import (
  16. AssembleSchema,
  17. ReleaseBase,
  18. ReleaseIn,
  19. ReleaseSchema,
  20. ReleaseUpdate,
  21. )
  22. router = Router()
  23. """
  24. POST /organizations/{organization_slug}/releases/
  25. POST /organizations/{organization_slug}/releases/{version}/deploys/ (Not implemented)
  26. GET /organizations/{organization_slug}/releases/
  27. GET /organizations/{organization_slug}/releases/{version}/
  28. PUT /organizations/{organization_slug}/releases/{version}/
  29. DELETE /organizations/{organization_slug}/releases/{version}/
  30. GET /organizations/{organization_slug}/releases/{version}/files/
  31. GET /organizations/{organization_slug}/releases/{version}/files/{file_id}/
  32. POST /organizations/{organization_slug}/releases/{version}/assemble/ (sentry undocumented)
  33. DELETE /organizations/{organization_slug}/releases/{version}/files/{file_id}/
  34. GET /projects/{organization_slug}/{project_slug}/releases/ (sentry undocumented)
  35. GET /projects/{organization_slug}/{project_slug}/releases/{version}/ (sentry undocumented)
  36. DELETE /projects/{organization_slug}/{project_slug}/releases/{version}/ (sentry undocumented)
  37. PUT /projects/organizations/{organization_slug}/releases/{version}/ (sentry undocumented)
  38. POST /projects/{organization_slug}/{project_slug}/releases/ (sentry undocumented)
  39. GET /projects/{organization_slug}/{project_slug}/releases/{version}/files/{file_id}/
  40. DELETE /projects/{organization_slug}/{project_slug}/releases/{version}/files/{file_id}/ (sentry undocumented)
  41. """
  42. def get_releases_queryset(
  43. organization_slug: str,
  44. user_id: int,
  45. id: int | None = None,
  46. version: str | None = None,
  47. project_slug: str | None = None,
  48. ):
  49. qs = Release.objects.filter(
  50. organization__slug=organization_slug, organization__users=user_id
  51. )
  52. if id:
  53. qs = qs.filter(id=id)
  54. if version:
  55. qs = qs.filter(version=version)
  56. if project_slug:
  57. qs = qs.filter(projects__slug=project_slug)
  58. return qs.prefetch_related("projects")
  59. def get_release_files_queryset(
  60. organization_slug: str,
  61. user_id: int,
  62. version: str | None = None,
  63. project_slug: str | None = None,
  64. id: int | None = None,
  65. ):
  66. qs = DebugSymbolBundle.objects.filter(
  67. release__organization__slug=organization_slug,
  68. release__organization__users=user_id,
  69. )
  70. if id:
  71. qs = qs.filter(id=id)
  72. if version:
  73. qs = qs.filter(release__version=version)
  74. if project_slug:
  75. qs = qs.filter(release__projects__slug=project_slug)
  76. return qs.select_related("file")
  77. @router.post(
  78. "/organizations/{slug:organization_slug}/releases/",
  79. response={201: ReleaseSchema},
  80. by_alias=True,
  81. )
  82. @has_permission(["project:releases"])
  83. async def create_release(
  84. request: AuthHttpRequest, organization_slug: str, payload: ReleaseIn
  85. ):
  86. user_id = request.auth.user_id
  87. organization = await aget_object_or_404(
  88. Organization, slug=organization_slug, users=user_id
  89. )
  90. data = payload.dict()
  91. projects = [
  92. project_id
  93. async for project_id in Project.objects.filter(
  94. slug__in=data.pop("projects"), organization=organization
  95. ).values_list("id", flat=True)
  96. ]
  97. if not projects:
  98. raise ValidationError([{"projects": "Require at least one valid project"}])
  99. release = await Release.objects.acreate(organization=organization, **data)
  100. await release.projects.aadd(*projects)
  101. return await get_releases_queryset(organization_slug, user_id, id=release.id).aget()
  102. @router.post(
  103. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/",
  104. response={201: ReleaseSchema},
  105. by_alias=True,
  106. )
  107. @has_permission(["project:releases"])
  108. async def create_project_release(
  109. request: AuthHttpRequest, organization_slug: str, project_slug, payload: ReleaseBase
  110. ):
  111. user_id = request.auth.user_id
  112. project = await aget_object_or_404(
  113. Project.objects.select_related("organization"),
  114. slug=project_slug,
  115. organization__slug=organization_slug,
  116. organization__users=user_id,
  117. )
  118. data = payload.dict()
  119. version = data.pop("version")
  120. release, _ = await Release.objects.aget_or_create(
  121. organization=project.organization, version=version, defaults=data
  122. )
  123. await release.projects.aadd(project)
  124. return await get_releases_queryset(organization_slug, user_id, id=release.id).aget()
  125. @router.get(
  126. "/organizations/{slug:organization_slug}/releases/",
  127. response=list[ReleaseSchema],
  128. by_alias=True,
  129. )
  130. @paginate
  131. @has_permission(["project:releases"])
  132. async def list_releases(
  133. request: AuthHttpRequest, response: HttpResponse, organization_slug: str
  134. ):
  135. return get_releases_queryset(organization_slug, request.auth.user_id)
  136. @router.get(
  137. "/organizations/{slug:organization_slug}/releases/{str:version}/",
  138. response=ReleaseSchema,
  139. by_alias=True,
  140. )
  141. @has_permission(["project:releases"])
  142. async def get_release(request: AuthHttpRequest, organization_slug: str, version: str):
  143. return await aget_object_or_404(
  144. get_releases_queryset(organization_slug, request.auth.user_id, version=version)
  145. )
  146. @router.put(
  147. "/organizations/{slug:organization_slug}/releases/{str:version}/",
  148. response=ReleaseSchema,
  149. by_alias=True,
  150. )
  151. @has_permission(["project:releases"])
  152. async def update_release(
  153. request: AuthHttpRequest,
  154. organization_slug: str,
  155. version: str,
  156. payload: ReleaseUpdate,
  157. ):
  158. user_id = request.auth.user_id
  159. release = await aget_object_or_404(
  160. get_releases_queryset(organization_slug, user_id, version=version)
  161. )
  162. for attr, value in payload.dict().items():
  163. setattr(release, attr, value)
  164. await release.asave()
  165. return await get_releases_queryset(organization_slug, user_id, id=release.id).aget()
  166. @router.delete(
  167. "/organizations/{slug:organization_slug}/releases/{str:version}/",
  168. response={204: None},
  169. )
  170. @has_permission(["project:releases"])
  171. async def delete_organization_release(
  172. request: AuthHttpRequest, organization_slug: str, version: str
  173. ):
  174. result, _ = await get_releases_queryset(
  175. organization_slug, request.auth.user_id, version=version
  176. ).adelete()
  177. if not result:
  178. raise Http404
  179. return 204, None
  180. @router.get(
  181. "/organizations/{slug:organization_slug}/releases/{str:version}/files/",
  182. response=list[DebugSymbolBundleSchema],
  183. by_alias=True,
  184. )
  185. @paginate
  186. @has_permission(["project:releases"])
  187. async def list_release_files(
  188. request: AuthHttpRequest,
  189. response: HttpResponse,
  190. organization_slug: str,
  191. version: str,
  192. ):
  193. return get_release_files_queryset(
  194. organization_slug,
  195. request.auth.user_id,
  196. version=version,
  197. )
  198. @router.get(
  199. "/organizations/{slug:organization_slug}/releases/{str:version}/files/{int:file_id}/",
  200. response=DebugSymbolBundleSchema,
  201. by_alias=True,
  202. )
  203. @has_permission(["project:releases"])
  204. async def get_organization_release_file(
  205. request: AuthHttpRequest,
  206. organization_slug: str,
  207. project_slug: str,
  208. version: str,
  209. file_id: int,
  210. ):
  211. return await aget_object_or_404(
  212. get_release_files_queryset(
  213. organization_slug,
  214. request.auth.user_id,
  215. project_slug=project_slug,
  216. version=version,
  217. id=file_id,
  218. )
  219. )
  220. @router.delete(
  221. "/organizations/{slug:organization_slug}/releases/{str:version}/files/{int:file_id}/",
  222. response={204: None},
  223. )
  224. @has_permission(["project:releases"])
  225. async def delete_organization_release_file(
  226. request: AuthHttpRequest, organization_slug: str, version: str, file_id: int
  227. ):
  228. result, _ = await get_release_files_queryset(
  229. organization_slug, request.auth.user_id, version=version, id=file_id
  230. ).adelete()
  231. if not result:
  232. raise Http404
  233. return 204, None
  234. @router.get(
  235. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/",
  236. response=list[ReleaseSchema],
  237. by_alias=True,
  238. )
  239. @paginate
  240. @has_permission(["project:releases"])
  241. async def list_project_releases(
  242. request: AuthHttpRequest,
  243. response: HttpResponse,
  244. organization_slug: str,
  245. project_slug: str,
  246. ):
  247. return get_releases_queryset(
  248. organization_slug, request.auth.user_id, project_slug=project_slug
  249. )
  250. @router.get(
  251. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/{str:version}/",
  252. response=ReleaseSchema,
  253. by_alias=True,
  254. )
  255. @has_permission(["project:releases"])
  256. async def get_project_release(
  257. request: AuthHttpRequest, organization_slug: str, project_slug: str, version: str
  258. ):
  259. return await aget_object_or_404(
  260. get_releases_queryset(
  261. organization_slug,
  262. request.auth.user_id,
  263. project_slug=project_slug,
  264. version=version,
  265. )
  266. )
  267. @router.put(
  268. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/{str:version}/",
  269. response=ReleaseSchema,
  270. by_alias=True,
  271. )
  272. @has_permission(["project:releases"])
  273. async def update_project_release(
  274. request: AuthHttpRequest,
  275. organization_slug: str,
  276. project_slug: str,
  277. version: str,
  278. payload: ReleaseUpdate,
  279. ):
  280. user_id = request.auth.user_id
  281. release = await aget_object_or_404(
  282. get_releases_queryset(
  283. organization_slug, user_id, version=version, project_slug=project_slug
  284. )
  285. )
  286. for attr, value in payload.dict().items():
  287. setattr(release, attr, value)
  288. await release.asave()
  289. return await get_releases_queryset(
  290. organization_slug, user_id, id=release.id, project_slug=project_slug
  291. ).aget()
  292. @router.delete(
  293. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/{str:version}/",
  294. response={204: None},
  295. )
  296. @has_permission(["project:releases"])
  297. async def delete_project_release(
  298. request: AuthHttpRequest, organization_slug: str, project_slug: str, version: str
  299. ):
  300. result, _ = await get_releases_queryset(
  301. organization_slug,
  302. request.auth.user_id,
  303. version=version,
  304. project_slug=project_slug,
  305. ).adelete()
  306. if not result:
  307. raise Http404
  308. return 204, None
  309. @router.get(
  310. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/{str:version}/files/",
  311. response=list[DebugSymbolBundleSchema],
  312. by_alias=True,
  313. )
  314. @paginate
  315. @has_permission(["project:releases"])
  316. async def list_project_release_files(
  317. request: AuthHttpRequest,
  318. response: HttpResponse,
  319. organization_slug: str,
  320. project_slug: str,
  321. version: str,
  322. ):
  323. return get_release_files_queryset(
  324. organization_slug,
  325. request.auth.user_id,
  326. project_slug=project_slug,
  327. version=version,
  328. )
  329. @router.delete(
  330. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/{str:version}/files/{int:file_id}/",
  331. response={204: None},
  332. )
  333. @has_permission(["project:releases"])
  334. async def delete_project_release_file(
  335. request: AuthHttpRequest,
  336. organization_slug: str,
  337. project_slug: str,
  338. version: str,
  339. file_id: int,
  340. ):
  341. result, _ = await get_release_files_queryset(
  342. organization_slug,
  343. request.auth.user_id,
  344. version=version,
  345. id=file_id,
  346. project_slug=project_slug,
  347. ).adelete()
  348. if not result:
  349. raise Http404
  350. return 204, None
  351. @router.get(
  352. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/{str:version}/files/{int:file_id}/",
  353. response=DebugSymbolBundleSchema,
  354. by_alias=True,
  355. )
  356. @has_permission(["project:releases"])
  357. async def get_project_release_file(
  358. request: AuthHttpRequest,
  359. organization_slug: str,
  360. project_slug: str,
  361. version: str,
  362. file_id: int,
  363. ):
  364. return await aget_object_or_404(
  365. get_release_files_queryset(
  366. organization_slug,
  367. request.auth.user_id,
  368. project_slug=project_slug,
  369. version=version,
  370. id=file_id,
  371. )
  372. )
  373. @router.post("/organizations/{slug:organization_slug}/releases/{str:version}/assemble/")
  374. @has_permission(["project:releases", "project:write", "project:admin"])
  375. async def assemble_release(
  376. request: AuthHttpRequest,
  377. organization_slug: str,
  378. version: str,
  379. payload: AssembleSchema,
  380. ):
  381. user_id = request.auth.user_id
  382. organization = await aget_object_or_404(
  383. Organization, slug=organization_slug, users=user_id
  384. )
  385. await async_call_celery_task(
  386. assemble_artifacts_task,
  387. organization.id,
  388. version,
  389. payload.checksum,
  390. payload.chunks,
  391. )
  392. # TODO should return more state's
  393. return {"state": "ok", "missingChunks": []}