api.py 12 KB

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