release_handler.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. package api
  2. import (
  3. "encoding/json"
  4. "net/http"
  5. "net/url"
  6. "strconv"
  7. "github.com/go-chi/chi"
  8. "github.com/porter-dev/porter/internal/forms"
  9. "github.com/porter-dev/porter/internal/helm"
  10. "github.com/porter-dev/porter/internal/helm/grapher"
  11. "github.com/porter-dev/porter/internal/kubernetes"
  12. "github.com/porter-dev/porter/internal/repository"
  13. )
  14. // Enumeration of release API error codes, represented as int64
  15. const (
  16. ErrReleaseDecode ErrorCode = iota + 600
  17. ErrReleaseValidateFields
  18. ErrReleaseReadData
  19. ErrReleaseDeploy
  20. )
  21. // HandleListReleases retrieves a list of releases for a cluster
  22. // with various filter options
  23. func (app *App) HandleListReleases(w http.ResponseWriter, r *http.Request) {
  24. form := &forms.ListReleaseForm{
  25. ReleaseForm: &forms.ReleaseForm{
  26. Form: &helm.Form{
  27. UpdateTokenCache: app.updateTokenCache,
  28. },
  29. },
  30. ListFilter: &helm.ListFilter{},
  31. }
  32. agent, err := app.getAgentFromQueryParams(
  33. w,
  34. r,
  35. form.ReleaseForm,
  36. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  37. form.PopulateListFromQueryParams,
  38. )
  39. // errors are handled in app.getAgentFromQueryParams
  40. if err != nil {
  41. return
  42. }
  43. releases, err := agent.ListReleases(form.Namespace, form.ListFilter)
  44. if err != nil {
  45. app.handleErrorRead(err, ErrReleaseReadData, w)
  46. return
  47. }
  48. if err := json.NewEncoder(w).Encode(releases); err != nil {
  49. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  50. return
  51. }
  52. }
  53. // HandleGetRelease retrieves a single release based on a name and revision
  54. func (app *App) HandleGetRelease(w http.ResponseWriter, r *http.Request) {
  55. name := chi.URLParam(r, "name")
  56. revision, err := strconv.ParseUint(chi.URLParam(r, "revision"), 0, 64)
  57. form := &forms.GetReleaseForm{
  58. ReleaseForm: &forms.ReleaseForm{
  59. Form: &helm.Form{
  60. UpdateTokenCache: app.updateTokenCache,
  61. },
  62. },
  63. Name: name,
  64. Revision: int(revision),
  65. }
  66. agent, err := app.getAgentFromQueryParams(
  67. w,
  68. r,
  69. form.ReleaseForm,
  70. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  71. )
  72. // errors are handled in app.getAgentFromQueryParams
  73. if err != nil {
  74. return
  75. }
  76. release, err := agent.GetRelease(form.Name, form.Revision)
  77. if err != nil {
  78. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  79. Code: ErrReleaseReadData,
  80. Errors: []string{"release not found"},
  81. }, w)
  82. return
  83. }
  84. if err := json.NewEncoder(w).Encode(release); err != nil {
  85. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  86. return
  87. }
  88. }
  89. // HandleGetReleaseComponents retrieves kubernetes objects listed in a release identified by name and revision
  90. func (app *App) HandleGetReleaseComponents(w http.ResponseWriter, r *http.Request) {
  91. name := chi.URLParam(r, "name")
  92. revision, err := strconv.ParseUint(chi.URLParam(r, "revision"), 0, 64)
  93. form := &forms.GetReleaseForm{
  94. ReleaseForm: &forms.ReleaseForm{
  95. Form: &helm.Form{
  96. UpdateTokenCache: app.updateTokenCache,
  97. },
  98. },
  99. Name: name,
  100. Revision: int(revision),
  101. }
  102. agent, err := app.getAgentFromQueryParams(
  103. w,
  104. r,
  105. form.ReleaseForm,
  106. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  107. )
  108. // errors are handled in app.getAgentFromQueryParams
  109. if err != nil {
  110. return
  111. }
  112. release, err := agent.GetRelease(form.Name, form.Revision)
  113. if err != nil {
  114. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  115. Code: ErrReleaseReadData,
  116. Errors: []string{"release not found"},
  117. }, w)
  118. return
  119. }
  120. yamlArr := grapher.ImportMultiDocYAML([]byte(release.Manifest))
  121. objects := grapher.ParseObjs(yamlArr)
  122. parsed := grapher.ParsedObjs{
  123. Objects: objects,
  124. }
  125. parsed.GetControlRel()
  126. parsed.GetLabelRel()
  127. parsed.GetSpecRel()
  128. if err := json.NewEncoder(w).Encode(parsed); err != nil {
  129. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  130. return
  131. }
  132. }
  133. // HandleGetReleaseControllers retrieves a single release based on a name and revision
  134. func (app *App) HandleGetReleaseControllers(w http.ResponseWriter, r *http.Request) {
  135. name := chi.URLParam(r, "name")
  136. revision, err := strconv.ParseUint(chi.URLParam(r, "revision"), 0, 64)
  137. form := &forms.GetReleaseForm{
  138. ReleaseForm: &forms.ReleaseForm{
  139. Form: &helm.Form{
  140. UpdateTokenCache: app.updateTokenCache,
  141. },
  142. },
  143. Name: name,
  144. Revision: int(revision),
  145. }
  146. agent, err := app.getAgentFromQueryParams(
  147. w,
  148. r,
  149. form.ReleaseForm,
  150. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  151. )
  152. // errors are handled in app.getAgentFromQueryParams
  153. if err != nil {
  154. return
  155. }
  156. release, err := agent.GetRelease(form.Name, form.Revision)
  157. if err != nil {
  158. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  159. Code: ErrReleaseReadData,
  160. Errors: []string{"release not found"},
  161. }, w)
  162. return
  163. }
  164. vals, err := url.ParseQuery(r.URL.RawQuery)
  165. if err != nil {
  166. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  167. return
  168. }
  169. // get the filter options
  170. k8sForm := &forms.K8sForm{
  171. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  172. UpdateTokenCache: app.updateTokenCache,
  173. },
  174. }
  175. k8sForm.PopulateK8sOptionsFromQueryParams(vals, app.repo.ServiceAccount)
  176. // validate the form
  177. if err := app.validator.Struct(k8sForm); err != nil {
  178. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  179. return
  180. }
  181. // create a new kubernetes agent
  182. var k8sAgent *kubernetes.Agent
  183. if app.testing {
  184. k8sAgent = app.TestAgents.K8sAgent
  185. } else {
  186. k8sAgent, err = kubernetes.GetAgentOutOfClusterConfig(k8sForm.OutOfClusterConfig)
  187. }
  188. yamlArr := grapher.ImportMultiDocYAML([]byte(release.Manifest))
  189. controllers := grapher.ParseControllers(yamlArr)
  190. retrievedControllers := []interface{}{}
  191. // get current status of each controller
  192. // TODO: refactor with type assertion
  193. for _, c := range controllers {
  194. switch c.Kind {
  195. case "Deployment":
  196. rc, err := k8sAgent.GetDeployment(c)
  197. if err != nil {
  198. app.handleErrorDataRead(err, w)
  199. return
  200. }
  201. rc.Kind = c.Kind
  202. retrievedControllers = append(retrievedControllers, rc)
  203. case "StatefulSet":
  204. rc, err := k8sAgent.GetStatefulSet(c)
  205. if err != nil {
  206. app.handleErrorDataRead(err, w)
  207. return
  208. }
  209. rc.Kind = c.Kind
  210. retrievedControllers = append(retrievedControllers, rc)
  211. case "DaemonSet":
  212. rc, err := k8sAgent.GetDaemonSet(c)
  213. if err != nil {
  214. app.handleErrorDataRead(err, w)
  215. return
  216. }
  217. rc.Kind = c.Kind
  218. retrievedControllers = append(retrievedControllers, rc)
  219. case "ReplicaSet":
  220. rc, err := k8sAgent.GetReplicaSet(c)
  221. if err != nil {
  222. app.handleErrorDataRead(err, w)
  223. return
  224. }
  225. rc.Kind = c.Kind
  226. retrievedControllers = append(retrievedControllers, rc)
  227. }
  228. }
  229. if err := json.NewEncoder(w).Encode(retrievedControllers); err != nil {
  230. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  231. return
  232. }
  233. }
  234. // HandleListReleaseHistory retrieves a history of releases based on a release name
  235. func (app *App) HandleListReleaseHistory(w http.ResponseWriter, r *http.Request) {
  236. name := chi.URLParam(r, "name")
  237. form := &forms.ListReleaseHistoryForm{
  238. ReleaseForm: &forms.ReleaseForm{
  239. Form: &helm.Form{
  240. UpdateTokenCache: app.updateTokenCache,
  241. },
  242. },
  243. Name: name,
  244. }
  245. agent, err := app.getAgentFromQueryParams(
  246. w,
  247. r,
  248. form.ReleaseForm,
  249. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  250. )
  251. // errors are handled in app.getAgentFromQueryParams
  252. if err != nil {
  253. return
  254. }
  255. release, err := agent.GetReleaseHistory(form.Name)
  256. if err != nil {
  257. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  258. Code: ErrReleaseReadData,
  259. Errors: []string{"release not found"},
  260. }, w)
  261. return
  262. }
  263. if err := json.NewEncoder(w).Encode(release); err != nil {
  264. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  265. return
  266. }
  267. }
  268. // HandleUpgradeRelease upgrades a release with new values.yaml
  269. func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
  270. name := chi.URLParam(r, "name")
  271. vals, err := url.ParseQuery(r.URL.RawQuery)
  272. if err != nil {
  273. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  274. return
  275. }
  276. form := &forms.UpgradeReleaseForm{
  277. ReleaseForm: &forms.ReleaseForm{
  278. Form: &helm.Form{
  279. UpdateTokenCache: app.updateTokenCache,
  280. },
  281. },
  282. Name: name,
  283. }
  284. form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
  285. vals,
  286. app.repo.ServiceAccount,
  287. )
  288. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  289. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  290. return
  291. }
  292. agent, err := app.getAgentFromReleaseForm(
  293. w,
  294. r,
  295. form.ReleaseForm,
  296. )
  297. // errors are handled in app.getAgentFromBodyParams
  298. if err != nil {
  299. return
  300. }
  301. _, err = agent.UpgradeRelease(form.Name, form.Values)
  302. if err != nil {
  303. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  304. Code: ErrReleaseDeploy,
  305. Errors: []string{"error upgrading release " + err.Error()},
  306. }, w)
  307. return
  308. }
  309. w.WriteHeader(http.StatusOK)
  310. }
  311. // HandleRollbackRelease rolls a release back to a specified revision
  312. func (app *App) HandleRollbackRelease(w http.ResponseWriter, r *http.Request) {
  313. name := chi.URLParam(r, "name")
  314. vals, err := url.ParseQuery(r.URL.RawQuery)
  315. if err != nil {
  316. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  317. return
  318. }
  319. form := &forms.RollbackReleaseForm{
  320. ReleaseForm: &forms.ReleaseForm{
  321. Form: &helm.Form{
  322. UpdateTokenCache: app.updateTokenCache,
  323. },
  324. },
  325. Name: name,
  326. }
  327. form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
  328. vals,
  329. app.repo.ServiceAccount,
  330. )
  331. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  332. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  333. return
  334. }
  335. agent, err := app.getAgentFromReleaseForm(
  336. w,
  337. r,
  338. form.ReleaseForm,
  339. )
  340. // errors are handled in app.getAgentFromBodyParams
  341. if err != nil {
  342. return
  343. }
  344. err = agent.RollbackRelease(form.Name, form.Revision)
  345. if err != nil {
  346. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  347. Code: ErrReleaseDeploy,
  348. Errors: []string{"error rolling back release " + err.Error()},
  349. }, w)
  350. return
  351. }
  352. w.WriteHeader(http.StatusOK)
  353. }
  354. // ------------------------ Release handler helper functions ------------------------ //
  355. // getAgentFromQueryParams uses the query params to populate a form, and then
  356. // passes that form to the underlying app.getAgentFromReleaseForm to create a new
  357. // Helm agent.
  358. func (app *App) getAgentFromQueryParams(
  359. w http.ResponseWriter,
  360. r *http.Request,
  361. form *forms.ReleaseForm,
  362. // populate uses the query params to populate a form
  363. populate ...func(vals url.Values, repo repository.ServiceAccountRepository) error,
  364. ) (*helm.Agent, error) {
  365. vals, err := url.ParseQuery(r.URL.RawQuery)
  366. if err != nil {
  367. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  368. return nil, err
  369. }
  370. for _, f := range populate {
  371. err := f(vals, app.repo.ServiceAccount)
  372. if err != nil {
  373. return nil, err
  374. }
  375. }
  376. return app.getAgentFromReleaseForm(w, r, form)
  377. }
  378. // getAgentFromReleaseForm uses a non-validated form to construct a new Helm agent based on
  379. // the userID found in the session and the options required by the Helm agent.
  380. func (app *App) getAgentFromReleaseForm(
  381. w http.ResponseWriter,
  382. r *http.Request,
  383. form *forms.ReleaseForm,
  384. ) (*helm.Agent, error) {
  385. var err error
  386. // validate the form
  387. if err := app.validator.Struct(form); err != nil {
  388. app.handleErrorFormValidation(err, ErrReleaseValidateFields, w)
  389. return nil, err
  390. }
  391. // create a new agent
  392. var agent *helm.Agent
  393. if app.testing {
  394. agent = app.TestAgents.HelmAgent
  395. } else {
  396. agent, err = helm.GetAgentOutOfClusterConfig(form.Form, app.logger)
  397. }
  398. return agent, err
  399. }