admin_pipeline_service.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. package service
  2. import (
  3. "context"
  4. "net/http"
  5. "strings"
  6. "cmr-backend/internal/apperr"
  7. "cmr-backend/internal/store/postgres"
  8. )
  9. type AdminPipelineService struct {
  10. store *postgres.Store
  11. configService *ConfigService
  12. }
  13. type AdminReleaseView struct {
  14. ID string `json:"id"`
  15. ReleaseNo int `json:"releaseNo"`
  16. ConfigLabel string `json:"configLabel"`
  17. ManifestURL string `json:"manifestUrl"`
  18. ManifestChecksumSha256 *string `json:"manifestChecksumSha256,omitempty"`
  19. RouteCode *string `json:"routeCode,omitempty"`
  20. BuildID *string `json:"buildId,omitempty"`
  21. Status string `json:"status"`
  22. PublishedAt string `json:"publishedAt"`
  23. Runtime *RuntimeSummaryView `json:"runtime,omitempty"`
  24. Presentation *PresentationSummaryView `json:"presentation,omitempty"`
  25. ContentBundle *ContentBundleSummaryView `json:"contentBundle,omitempty"`
  26. }
  27. type AdminEventPipelineView struct {
  28. EventID string `json:"eventId"`
  29. CurrentRelease *AdminReleaseView `json:"currentRelease,omitempty"`
  30. Sources []EventConfigSourceView `json:"sources"`
  31. Builds []EventConfigBuildView `json:"builds"`
  32. Releases []AdminReleaseView `json:"releases"`
  33. }
  34. type AdminRollbackReleaseInput struct {
  35. ReleaseID string `json:"releaseId"`
  36. }
  37. type AdminBindReleaseRuntimeInput struct {
  38. RuntimeBindingID string `json:"runtimeBindingId"`
  39. }
  40. type AdminPublishBuildInput struct {
  41. RuntimeBindingID string `json:"runtimeBindingId,omitempty"`
  42. PresentationID string `json:"presentationId,omitempty"`
  43. ContentBundleID string `json:"contentBundleId,omitempty"`
  44. }
  45. func NewAdminPipelineService(store *postgres.Store, configService *ConfigService) *AdminPipelineService {
  46. return &AdminPipelineService{
  47. store: store,
  48. configService: configService,
  49. }
  50. }
  51. func (s *AdminPipelineService) GetEventPipeline(ctx context.Context, eventPublicID string, limit int) (*AdminEventPipelineView, error) {
  52. event, err := s.store.GetEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
  53. if err != nil {
  54. return nil, err
  55. }
  56. if event == nil {
  57. return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
  58. }
  59. sources, err := s.configService.ListEventConfigSources(ctx, event.PublicID, limit)
  60. if err != nil {
  61. return nil, err
  62. }
  63. buildRecords, err := s.store.ListEventConfigBuildsByEventID(ctx, event.ID, limit)
  64. if err != nil {
  65. return nil, err
  66. }
  67. releaseRecords, err := s.store.ListEventReleasesByEventID(ctx, event.ID, limit)
  68. if err != nil {
  69. return nil, err
  70. }
  71. builds := make([]EventConfigBuildView, 0, len(buildRecords))
  72. for i := range buildRecords {
  73. item, err := buildEventConfigBuildView(&buildRecords[i])
  74. if err != nil {
  75. return nil, err
  76. }
  77. builds = append(builds, *item)
  78. }
  79. releases := make([]AdminReleaseView, 0, len(releaseRecords))
  80. for _, item := range releaseRecords {
  81. view := buildAdminReleaseView(item)
  82. if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, item.PresentationID); err != nil {
  83. return nil, err
  84. } else if enrichedPresentation != nil {
  85. view.Presentation = enrichedPresentation
  86. }
  87. if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, item.ContentBundleID); err != nil {
  88. return nil, err
  89. } else if enrichedBundle != nil {
  90. view.ContentBundle = enrichedBundle
  91. }
  92. releases = append(releases, view)
  93. }
  94. result := &AdminEventPipelineView{
  95. EventID: event.PublicID,
  96. Sources: sources,
  97. Builds: builds,
  98. Releases: releases,
  99. }
  100. if event.CurrentReleasePubID != nil {
  101. result.CurrentRelease = &AdminReleaseView{
  102. ID: *event.CurrentReleasePubID,
  103. ConfigLabel: derefStringOrEmpty(event.ConfigLabel),
  104. ManifestURL: derefStringOrEmpty(event.ManifestURL),
  105. ManifestChecksumSha256: event.ManifestChecksum,
  106. RouteCode: event.RouteCode,
  107. Status: "published",
  108. Runtime: buildRuntimeSummaryFromEvent(event),
  109. Presentation: buildPresentationSummaryFromEvent(event),
  110. ContentBundle: buildContentBundleSummaryFromEvent(event),
  111. }
  112. if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, event.PresentationID); err != nil {
  113. return nil, err
  114. } else if enrichedPresentation != nil {
  115. result.CurrentRelease.Presentation = enrichedPresentation
  116. }
  117. if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, event.ContentBundleID); err != nil {
  118. return nil, err
  119. } else if enrichedBundle != nil {
  120. result.CurrentRelease.ContentBundle = enrichedBundle
  121. }
  122. }
  123. return result, nil
  124. }
  125. func (s *AdminPipelineService) BuildSource(ctx context.Context, sourceID string) (*EventConfigBuildView, error) {
  126. return s.configService.BuildPreview(ctx, BuildPreviewInput{SourceID: sourceID})
  127. }
  128. func (s *AdminPipelineService) GetBuild(ctx context.Context, buildID string) (*EventConfigBuildView, error) {
  129. return s.configService.GetEventConfigBuild(ctx, buildID)
  130. }
  131. func (s *AdminPipelineService) PublishBuild(ctx context.Context, buildID string, input AdminPublishBuildInput) (*PublishedReleaseView, error) {
  132. return s.configService.PublishBuild(ctx, PublishBuildInput{
  133. BuildID: buildID,
  134. RuntimeBindingID: input.RuntimeBindingID,
  135. PresentationID: input.PresentationID,
  136. ContentBundleID: input.ContentBundleID,
  137. })
  138. }
  139. func (s *AdminPipelineService) GetRelease(ctx context.Context, releasePublicID string) (*AdminReleaseView, error) {
  140. release, err := s.store.GetEventReleaseByPublicID(ctx, strings.TrimSpace(releasePublicID))
  141. if err != nil {
  142. return nil, err
  143. }
  144. if release == nil {
  145. return nil, apperr.New(http.StatusNotFound, "release_not_found", "release not found")
  146. }
  147. view := buildAdminReleaseView(*release)
  148. if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, release.PresentationID); err != nil {
  149. return nil, err
  150. } else if enrichedPresentation != nil {
  151. view.Presentation = enrichedPresentation
  152. }
  153. if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, release.ContentBundleID); err != nil {
  154. return nil, err
  155. } else if enrichedBundle != nil {
  156. view.ContentBundle = enrichedBundle
  157. }
  158. return &view, nil
  159. }
  160. func (s *AdminPipelineService) BindReleaseRuntime(ctx context.Context, releasePublicID string, input AdminBindReleaseRuntimeInput) (*AdminReleaseView, error) {
  161. release, err := s.store.GetEventReleaseByPublicID(ctx, strings.TrimSpace(releasePublicID))
  162. if err != nil {
  163. return nil, err
  164. }
  165. if release == nil {
  166. return nil, apperr.New(http.StatusNotFound, "release_not_found", "release not found")
  167. }
  168. input.RuntimeBindingID = strings.TrimSpace(input.RuntimeBindingID)
  169. if input.RuntimeBindingID == "" {
  170. return nil, apperr.New(http.StatusBadRequest, "invalid_params", "runtimeBindingId is required")
  171. }
  172. runtimeBinding, err := s.store.GetMapRuntimeBindingByPublicID(ctx, input.RuntimeBindingID)
  173. if err != nil {
  174. return nil, err
  175. }
  176. if runtimeBinding == nil {
  177. return nil, apperr.New(http.StatusNotFound, "runtime_binding_not_found", "runtime binding not found")
  178. }
  179. if runtimeBinding.EventID != release.EventID {
  180. return nil, apperr.New(http.StatusConflict, "runtime_binding_not_belong_to_event", "runtime binding does not belong to release event")
  181. }
  182. tx, err := s.store.Begin(ctx)
  183. if err != nil {
  184. return nil, err
  185. }
  186. defer tx.Rollback(ctx)
  187. if err := s.store.SetEventReleaseRuntimeBinding(ctx, tx, release.ID, &runtimeBinding.ID); err != nil {
  188. return nil, err
  189. }
  190. if err := tx.Commit(ctx); err != nil {
  191. return nil, err
  192. }
  193. updated, err := s.store.GetEventReleaseByPublicID(ctx, release.PublicID)
  194. if err != nil {
  195. return nil, err
  196. }
  197. if updated == nil {
  198. return nil, apperr.New(http.StatusNotFound, "release_not_found", "release not found")
  199. }
  200. view := buildAdminReleaseView(*updated)
  201. return &view, nil
  202. }
  203. func (s *AdminPipelineService) RollbackRelease(ctx context.Context, eventPublicID string, input AdminRollbackReleaseInput) (*AdminReleaseView, error) {
  204. event, err := s.store.GetEventByPublicID(ctx, strings.TrimSpace(eventPublicID))
  205. if err != nil {
  206. return nil, err
  207. }
  208. if event == nil {
  209. return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
  210. }
  211. input.ReleaseID = strings.TrimSpace(input.ReleaseID)
  212. if input.ReleaseID == "" {
  213. return nil, apperr.New(http.StatusBadRequest, "invalid_params", "releaseId is required")
  214. }
  215. release, err := s.store.GetEventReleaseByPublicID(ctx, input.ReleaseID)
  216. if err != nil {
  217. return nil, err
  218. }
  219. if release == nil {
  220. return nil, apperr.New(http.StatusNotFound, "release_not_found", "release not found")
  221. }
  222. if release.EventID != event.ID {
  223. return nil, apperr.New(http.StatusConflict, "release_not_belong_to_event", "release does not belong to event")
  224. }
  225. if release.Status != "published" {
  226. return nil, apperr.New(http.StatusConflict, "release_not_publishable", "release is not published")
  227. }
  228. tx, err := s.store.Begin(ctx)
  229. if err != nil {
  230. return nil, err
  231. }
  232. defer tx.Rollback(ctx)
  233. if err := s.store.SetCurrentEventRelease(ctx, tx, event.ID, release.ID); err != nil {
  234. return nil, err
  235. }
  236. if err := tx.Commit(ctx); err != nil {
  237. return nil, err
  238. }
  239. view := buildAdminReleaseView(*release)
  240. return &view, nil
  241. }
  242. func buildAdminReleaseView(item postgres.EventRelease) AdminReleaseView {
  243. return AdminReleaseView{
  244. ID: item.PublicID,
  245. ReleaseNo: item.ReleaseNo,
  246. ConfigLabel: item.ConfigLabel,
  247. ManifestURL: item.ManifestURL,
  248. ManifestChecksumSha256: item.ManifestChecksum,
  249. RouteCode: item.RouteCode,
  250. BuildID: item.BuildID,
  251. Status: item.Status,
  252. PublishedAt: item.PublishedAt.Format(timeRFC3339),
  253. Runtime: buildRuntimeSummaryFromRelease(&item),
  254. Presentation: buildPresentationSummaryFromRelease(&item),
  255. ContentBundle: buildContentBundleSummaryFromRelease(&item),
  256. }
  257. }
  258. func derefStringOrEmpty(value *string) string {
  259. if value == nil {
  260. return ""
  261. }
  262. return *value
  263. }