release_handler.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  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. Repo: app.repo,
  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. Repo: app.repo,
  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. Repo: app.repo,
  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 controllers that belong to a release.
  134. // Used to display status of charts.
  135. func (app *App) HandleGetReleaseControllers(w http.ResponseWriter, r *http.Request) {
  136. name := chi.URLParam(r, "name")
  137. revision, err := strconv.ParseUint(chi.URLParam(r, "revision"), 0, 64)
  138. form := &forms.GetReleaseForm{
  139. ReleaseForm: &forms.ReleaseForm{
  140. Form: &helm.Form{
  141. Repo: app.repo,
  142. },
  143. },
  144. Name: name,
  145. Revision: int(revision),
  146. }
  147. agent, err := app.getAgentFromQueryParams(
  148. w,
  149. r,
  150. form.ReleaseForm,
  151. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  152. )
  153. // errors are handled in app.getAgentFromQueryParams
  154. if err != nil {
  155. return
  156. }
  157. release, err := agent.GetRelease(form.Name, form.Revision)
  158. if err != nil {
  159. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  160. Code: ErrReleaseReadData,
  161. Errors: []string{"release not found"},
  162. }, w)
  163. return
  164. }
  165. vals, err := url.ParseQuery(r.URL.RawQuery)
  166. if err != nil {
  167. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  168. return
  169. }
  170. // get the filter options
  171. k8sForm := &forms.K8sForm{
  172. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  173. Repo: app.repo,
  174. },
  175. }
  176. k8sForm.PopulateK8sOptionsFromQueryParams(vals, app.repo.Cluster)
  177. // validate the form
  178. if err := app.validator.Struct(k8sForm); err != nil {
  179. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  180. return
  181. }
  182. // create a new kubernetes agent
  183. var k8sAgent *kubernetes.Agent
  184. if app.testing {
  185. k8sAgent = app.TestAgents.K8sAgent
  186. } else {
  187. k8sAgent, err = kubernetes.GetAgentOutOfClusterConfig(k8sForm.OutOfClusterConfig)
  188. }
  189. yamlArr := grapher.ImportMultiDocYAML([]byte(release.Manifest))
  190. controllers := grapher.ParseControllers(yamlArr)
  191. retrievedControllers := []interface{}{}
  192. // get current status of each controller
  193. // TODO: refactor with type assertion
  194. for _, c := range controllers {
  195. switch c.Kind {
  196. case "Deployment":
  197. rc, err := k8sAgent.GetDeployment(c)
  198. if err != nil {
  199. app.handleErrorDataRead(err, w)
  200. return
  201. }
  202. rc.Kind = c.Kind
  203. retrievedControllers = append(retrievedControllers, rc)
  204. case "StatefulSet":
  205. rc, err := k8sAgent.GetStatefulSet(c)
  206. if err != nil {
  207. app.handleErrorDataRead(err, w)
  208. return
  209. }
  210. rc.Kind = c.Kind
  211. retrievedControllers = append(retrievedControllers, rc)
  212. case "DaemonSet":
  213. rc, err := k8sAgent.GetDaemonSet(c)
  214. if err != nil {
  215. app.handleErrorDataRead(err, w)
  216. return
  217. }
  218. rc.Kind = c.Kind
  219. retrievedControllers = append(retrievedControllers, rc)
  220. case "ReplicaSet":
  221. rc, err := k8sAgent.GetReplicaSet(c)
  222. if err != nil {
  223. app.handleErrorDataRead(err, w)
  224. return
  225. }
  226. rc.Kind = c.Kind
  227. retrievedControllers = append(retrievedControllers, rc)
  228. }
  229. }
  230. if err := json.NewEncoder(w).Encode(retrievedControllers); err != nil {
  231. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  232. return
  233. }
  234. }
  235. // HandleListReleaseHistory retrieves a history of releases based on a release name
  236. func (app *App) HandleListReleaseHistory(w http.ResponseWriter, r *http.Request) {
  237. name := chi.URLParam(r, "name")
  238. form := &forms.ListReleaseHistoryForm{
  239. ReleaseForm: &forms.ReleaseForm{
  240. Form: &helm.Form{
  241. Repo: app.repo,
  242. },
  243. },
  244. Name: name,
  245. }
  246. agent, err := app.getAgentFromQueryParams(
  247. w,
  248. r,
  249. form.ReleaseForm,
  250. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  251. )
  252. // errors are handled in app.getAgentFromQueryParams
  253. if err != nil {
  254. return
  255. }
  256. release, err := agent.GetReleaseHistory(form.Name)
  257. if err != nil {
  258. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  259. Code: ErrReleaseReadData,
  260. Errors: []string{"release not found"},
  261. }, w)
  262. return
  263. }
  264. if err := json.NewEncoder(w).Encode(release); err != nil {
  265. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  266. return
  267. }
  268. }
  269. // HandleUpgradeRelease upgrades a release with new values.yaml
  270. func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
  271. name := chi.URLParam(r, "name")
  272. vals, err := url.ParseQuery(r.URL.RawQuery)
  273. if err != nil {
  274. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  275. return
  276. }
  277. form := &forms.UpgradeReleaseForm{
  278. ReleaseForm: &forms.ReleaseForm{
  279. Form: &helm.Form{
  280. Repo: app.repo,
  281. },
  282. },
  283. Name: name,
  284. }
  285. form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
  286. vals,
  287. app.repo.Cluster,
  288. )
  289. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  290. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  291. return
  292. }
  293. agent, err := app.getAgentFromReleaseForm(
  294. w,
  295. r,
  296. form.ReleaseForm,
  297. )
  298. // errors are handled in app.getAgentFromBodyParams
  299. if err != nil {
  300. return
  301. }
  302. _, err = agent.UpgradeRelease(form.Name, form.Values)
  303. if err != nil {
  304. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  305. Code: ErrReleaseDeploy,
  306. Errors: []string{"error upgrading release " + err.Error()},
  307. }, w)
  308. return
  309. }
  310. w.WriteHeader(http.StatusOK)
  311. }
  312. // HandleRollbackRelease rolls a release back to a specified revision
  313. func (app *App) HandleRollbackRelease(w http.ResponseWriter, r *http.Request) {
  314. name := chi.URLParam(r, "name")
  315. vals, err := url.ParseQuery(r.URL.RawQuery)
  316. if err != nil {
  317. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  318. return
  319. }
  320. form := &forms.RollbackReleaseForm{
  321. ReleaseForm: &forms.ReleaseForm{
  322. Form: &helm.Form{
  323. Repo: app.repo,
  324. },
  325. },
  326. Name: name,
  327. }
  328. form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
  329. vals,
  330. app.repo.Cluster,
  331. )
  332. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  333. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  334. return
  335. }
  336. agent, err := app.getAgentFromReleaseForm(
  337. w,
  338. r,
  339. form.ReleaseForm,
  340. )
  341. // errors are handled in app.getAgentFromBodyParams
  342. if err != nil {
  343. return
  344. }
  345. err = agent.RollbackRelease(form.Name, form.Revision)
  346. if err != nil {
  347. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  348. Code: ErrReleaseDeploy,
  349. Errors: []string{"error rolling back release " + err.Error()},
  350. }, w)
  351. return
  352. }
  353. w.WriteHeader(http.StatusOK)
  354. }
  355. // ------------------------ Release handler helper functions ------------------------ //
  356. // getAgentFromQueryParams uses the query params to populate a form, and then
  357. // passes that form to the underlying app.getAgentFromReleaseForm to create a new
  358. // Helm agent.
  359. func (app *App) getAgentFromQueryParams(
  360. w http.ResponseWriter,
  361. r *http.Request,
  362. form *forms.ReleaseForm,
  363. // populate uses the query params to populate a form
  364. populate ...func(vals url.Values, repo repository.ClusterRepository) error,
  365. ) (*helm.Agent, error) {
  366. vals, err := url.ParseQuery(r.URL.RawQuery)
  367. if err != nil {
  368. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  369. return nil, err
  370. }
  371. for _, f := range populate {
  372. err := f(vals, app.repo.Cluster)
  373. if err != nil {
  374. return nil, err
  375. }
  376. }
  377. return app.getAgentFromReleaseForm(w, r, form)
  378. }
  379. // getAgentFromReleaseForm uses a non-validated form to construct a new Helm agent based on
  380. // the userID found in the session and the options required by the Helm agent.
  381. func (app *App) getAgentFromReleaseForm(
  382. w http.ResponseWriter,
  383. r *http.Request,
  384. form *forms.ReleaseForm,
  385. ) (*helm.Agent, error) {
  386. var err error
  387. // validate the form
  388. if err := app.validator.Struct(form); err != nil {
  389. app.handleErrorFormValidation(err, ErrReleaseValidateFields, w)
  390. return nil, err
  391. }
  392. // create a new agent
  393. var agent *helm.Agent
  394. if app.testing {
  395. agent = app.TestAgents.HelmAgent
  396. } else {
  397. agent, err = helm.GetAgentOutOfClusterConfig(form.Form, app.logger)
  398. }
  399. return agent, err
  400. }