deploy_handler.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. package api
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "net/url"
  7. "strconv"
  8. "strings"
  9. "gorm.io/gorm"
  10. "github.com/go-chi/chi"
  11. "github.com/porter-dev/porter/internal/analytics"
  12. "github.com/porter-dev/porter/internal/forms"
  13. "github.com/porter-dev/porter/internal/helm"
  14. "github.com/porter-dev/porter/internal/helm/loader"
  15. "github.com/porter-dev/porter/internal/integrations/ci/actions"
  16. "github.com/porter-dev/porter/internal/models"
  17. "github.com/porter-dev/porter/internal/oauth"
  18. "github.com/porter-dev/porter/internal/repository"
  19. "gopkg.in/yaml.v2"
  20. )
  21. // HandleDeployTemplate triggers a chart deployment from a template
  22. func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
  23. projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
  24. userID, err := app.getUserIDFromRequest(r)
  25. flowID := oauth.CreateRandomState()
  26. if err != nil || projID == 0 {
  27. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  28. return
  29. }
  30. name := chi.URLParam(r, "name")
  31. version := chi.URLParam(r, "version")
  32. // if version passed as latest, pass empty string to loader to get latest
  33. if version == "latest" {
  34. version = ""
  35. }
  36. getChartForm := &forms.ChartForm{
  37. Name: name,
  38. Version: version,
  39. RepoURL: app.ServerConf.DefaultApplicationHelmRepoURL,
  40. }
  41. // if a repo_url is passed as query param, it will be populated
  42. vals, err := url.ParseQuery(r.URL.RawQuery)
  43. if err != nil {
  44. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  45. return
  46. }
  47. clusterID, err := strconv.ParseUint(vals["cluster_id"][0], 10, 64)
  48. if err != nil {
  49. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  50. return
  51. }
  52. app.AnalyticsClient.Track(analytics.ApplicationLaunchStartTrack(
  53. &analytics.ApplicationLaunchStartTrackOpts{
  54. ClusterScopedTrackOpts: analytics.GetClusterScopedTrackOpts(userID, uint(projID), uint(clusterID)),
  55. FlowID: flowID,
  56. },
  57. ))
  58. getChartForm.PopulateRepoURLFromQueryParams(vals)
  59. chart, err := loader.LoadChartPublic(getChartForm.RepoURL, getChartForm.Name, getChartForm.Version)
  60. if err != nil {
  61. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  62. return
  63. }
  64. form := &forms.InstallChartTemplateForm{
  65. ReleaseForm: &forms.ReleaseForm{
  66. Form: &helm.Form{
  67. Repo: app.Repo,
  68. DigitalOceanOAuth: app.DOConf,
  69. },
  70. },
  71. ChartTemplateForm: &forms.ChartTemplateForm{},
  72. }
  73. form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
  74. vals,
  75. app.Repo.Cluster(),
  76. )
  77. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  78. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  79. return
  80. }
  81. agent, err := app.getAgentFromReleaseForm(
  82. w,
  83. r,
  84. form.ReleaseForm,
  85. )
  86. if err != nil {
  87. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  88. return
  89. }
  90. registries, err := app.Repo.Registry().ListRegistriesByProjectID(uint(projID))
  91. if err != nil {
  92. app.handleErrorDataRead(err, w)
  93. return
  94. }
  95. conf := &helm.InstallChartConfig{
  96. Chart: chart,
  97. Name: form.ChartTemplateForm.Name,
  98. Namespace: form.ReleaseForm.Form.Namespace,
  99. Values: form.ChartTemplateForm.FormValues,
  100. Cluster: form.ReleaseForm.Cluster,
  101. Repo: app.Repo,
  102. Registries: registries,
  103. }
  104. rel, err := agent.InstallChart(conf, app.DOConf)
  105. if err != nil {
  106. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  107. Code: ErrReleaseDeploy,
  108. Errors: []string{"error installing a new chart: " + err.Error()},
  109. }, w)
  110. return
  111. }
  112. token, err := repository.GenerateRandomBytes(16)
  113. if err != nil {
  114. app.handleErrorInternal(err, w)
  115. return
  116. }
  117. // create release with webhook token in db
  118. image, ok := rel.Config["image"].(map[string]interface{})
  119. if !ok {
  120. app.handleErrorInternal(fmt.Errorf("Could not find field image in config"), w)
  121. return
  122. }
  123. repository := image["repository"]
  124. repoStr, ok := repository.(string)
  125. if !ok {
  126. app.handleErrorInternal(fmt.Errorf("Could not find field repository in config"), w)
  127. return
  128. }
  129. release := &models.Release{
  130. ClusterID: form.ReleaseForm.Form.Cluster.ID,
  131. ProjectID: form.ReleaseForm.Form.Cluster.ProjectID,
  132. Namespace: form.ReleaseForm.Form.Namespace,
  133. Name: form.ChartTemplateForm.Name,
  134. WebhookToken: token,
  135. ImageRepoURI: repoStr,
  136. }
  137. _, err = app.Repo.Release().CreateRelease(release)
  138. if err != nil {
  139. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  140. Code: ErrReleaseDeploy,
  141. Errors: []string{"error creating a webhook: " + err.Error()},
  142. }, w)
  143. }
  144. // if github action config is linked, call the github action config handler
  145. if form.GithubActionConfig != nil {
  146. gaForm := &forms.CreateGitAction{
  147. Release: release,
  148. GitRepo: form.GithubActionConfig.GitRepo,
  149. GitBranch: form.GithubActionConfig.GitBranch,
  150. ImageRepoURI: form.GithubActionConfig.ImageRepoURI,
  151. DockerfilePath: form.GithubActionConfig.DockerfilePath,
  152. GitRepoID: form.GithubActionConfig.GitRepoID,
  153. RegistryID: form.GithubActionConfig.RegistryID,
  154. ShouldGenerateOnly: false,
  155. ShouldCreateWorkflow: form.GithubActionConfig.ShouldCreateWorkflow,
  156. }
  157. // validate the form
  158. if err := app.validator.Struct(form); err != nil {
  159. app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
  160. return
  161. }
  162. app.createGitActionFromForm(projID, clusterID, form.ChartTemplateForm.Name, form.ReleaseForm.Form.Namespace, gaForm, w, r)
  163. }
  164. app.AnalyticsClient.Track(analytics.ApplicationLaunchSuccessTrack(
  165. &analytics.ApplicationLaunchSuccessTrackOpts{
  166. ApplicationScopedTrackOpts: analytics.GetApplicationScopedTrackOpts(
  167. userID,
  168. uint(projID),
  169. uint(clusterID),
  170. release.Name,
  171. release.Namespace,
  172. chart.Metadata.Name,
  173. ),
  174. FlowID: flowID,
  175. },
  176. ))
  177. w.WriteHeader(http.StatusOK)
  178. }
  179. // HandleDeployAddon triggers a addon deployment from a template
  180. func (app *App) HandleDeployAddon(w http.ResponseWriter, r *http.Request) {
  181. projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
  182. userID, err := app.getUserIDFromRequest(r)
  183. flowID := oauth.CreateRandomState()
  184. if err != nil || projID == 0 {
  185. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  186. return
  187. }
  188. name := chi.URLParam(r, "name")
  189. version := chi.URLParam(r, "version")
  190. // if version passed as latest, pass empty string to loader to get latest
  191. if version == "latest" {
  192. version = ""
  193. }
  194. getChartForm := &forms.ChartForm{
  195. Name: name,
  196. Version: version,
  197. RepoURL: app.ServerConf.DefaultApplicationHelmRepoURL,
  198. }
  199. // if a repo_url is passed as query param, it will be populated
  200. vals, err := url.ParseQuery(r.URL.RawQuery)
  201. if err != nil {
  202. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  203. return
  204. }
  205. getChartForm.PopulateRepoURLFromQueryParams(vals)
  206. chart, err := loader.LoadChartPublic(getChartForm.RepoURL, getChartForm.Name, getChartForm.Version)
  207. if err != nil {
  208. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  209. return
  210. }
  211. form := &forms.InstallChartTemplateForm{
  212. ReleaseForm: &forms.ReleaseForm{
  213. Form: &helm.Form{
  214. Repo: app.Repo,
  215. DigitalOceanOAuth: app.DOConf,
  216. },
  217. },
  218. ChartTemplateForm: &forms.ChartTemplateForm{},
  219. }
  220. form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
  221. vals,
  222. app.Repo.Cluster(),
  223. )
  224. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  225. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  226. return
  227. }
  228. app.AnalyticsClient.Track(analytics.ApplicationLaunchStartTrack(
  229. &analytics.ApplicationLaunchStartTrackOpts{
  230. ClusterScopedTrackOpts: analytics.GetClusterScopedTrackOpts(userID, uint(projID), uint(form.ReleaseForm.Cluster.ID)),
  231. FlowID: flowID,
  232. },
  233. ))
  234. agent, err := app.getAgentFromReleaseForm(
  235. w,
  236. r,
  237. form.ReleaseForm,
  238. )
  239. if err != nil {
  240. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  241. return
  242. }
  243. registries, err := app.Repo.Registry().ListRegistriesByProjectID(uint(projID))
  244. if err != nil {
  245. app.handleErrorDataRead(err, w)
  246. return
  247. }
  248. conf := &helm.InstallChartConfig{
  249. Chart: chart,
  250. Name: form.ChartTemplateForm.Name,
  251. Namespace: form.ReleaseForm.Form.Namespace,
  252. Values: form.ChartTemplateForm.FormValues,
  253. Cluster: form.ReleaseForm.Cluster,
  254. Repo: app.Repo,
  255. Registries: registries,
  256. }
  257. rel, err := agent.InstallChart(conf, app.DOConf)
  258. if err != nil {
  259. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  260. Code: ErrReleaseDeploy,
  261. Errors: []string{"error installing a new chart: " + err.Error()},
  262. }, w)
  263. return
  264. }
  265. app.AnalyticsClient.Track(analytics.ApplicationLaunchSuccessTrack(
  266. &analytics.ApplicationLaunchSuccessTrackOpts{
  267. ApplicationScopedTrackOpts: analytics.GetApplicationScopedTrackOpts(
  268. userID,
  269. uint(projID),
  270. uint(form.ReleaseForm.Cluster.ID),
  271. rel.Name,
  272. rel.Namespace,
  273. chart.Metadata.Name,
  274. ),
  275. FlowID: flowID,
  276. },
  277. ))
  278. w.WriteHeader(http.StatusOK)
  279. }
  280. // HandleUninstallTemplate triggers a chart deployment from a template
  281. func (app *App) HandleUninstallTemplate(w http.ResponseWriter, r *http.Request) {
  282. name := chi.URLParam(r, "name")
  283. vals, err := url.ParseQuery(r.URL.RawQuery)
  284. if err != nil {
  285. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  286. return
  287. }
  288. form := &forms.GetReleaseForm{
  289. ReleaseForm: &forms.ReleaseForm{
  290. Form: &helm.Form{
  291. Repo: app.Repo,
  292. DigitalOceanOAuth: app.DOConf,
  293. },
  294. },
  295. Name: name,
  296. }
  297. agent, err := app.getAgentFromQueryParams(
  298. w,
  299. r,
  300. form.ReleaseForm,
  301. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  302. )
  303. // errors are handled in app.getAgentFromQueryParams
  304. if err != nil {
  305. return
  306. }
  307. resp, err := agent.UninstallChart(name)
  308. if err != nil {
  309. return
  310. }
  311. // update the github actions env if the release exists and is built from source
  312. if cName := resp.Release.Chart.Metadata.Name; cName == "job" || cName == "web" || cName == "worker" {
  313. clusterID, err := strconv.ParseUint(vals["cluster_id"][0], 10, 64)
  314. if err != nil {
  315. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  316. Code: ErrReleaseReadData,
  317. Errors: []string{"release not found"},
  318. }, w)
  319. }
  320. release, err := app.Repo.Release().ReadRelease(uint(clusterID), name, resp.Release.Namespace)
  321. if release != nil {
  322. gitAction := release.GitActionConfig
  323. if gitAction.ID != 0 {
  324. // parse env into build env
  325. cEnv := &ContainerEnvConfig{}
  326. rawValues, err := yaml.Marshal(resp.Release.Config)
  327. if err != nil {
  328. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  329. Code: ErrReleaseReadData,
  330. Errors: []string{"could not get values of previous revision"},
  331. }, w)
  332. }
  333. yaml.Unmarshal(rawValues, cEnv)
  334. gr, err := app.Repo.GitRepo().ReadGitRepo(gitAction.GitRepoID)
  335. if err != nil {
  336. if err != gorm.ErrRecordNotFound {
  337. app.handleErrorInternal(err, w)
  338. return
  339. }
  340. gr = nil
  341. }
  342. repoSplit := strings.Split(gitAction.GitRepo, "/")
  343. projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
  344. if err != nil || projID == 0 {
  345. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  346. return
  347. }
  348. gaRunner := &actions.GithubActions{
  349. ServerURL: app.ServerConf.ServerURL,
  350. GithubOAuthIntegration: gr,
  351. GithubAppID: app.GithubAppConf.AppID,
  352. GithubAppSecretPath: app.GithubAppConf.SecretPath,
  353. GithubInstallationID: gitAction.GithubInstallationID,
  354. GitRepoName: repoSplit[1],
  355. GitRepoOwner: repoSplit[0],
  356. Repo: app.Repo,
  357. GithubConf: app.GithubProjectConf,
  358. ProjectID: uint(projID),
  359. ReleaseName: name,
  360. ReleaseNamespace: release.Namespace,
  361. GitBranch: gitAction.GitBranch,
  362. DockerFilePath: gitAction.DockerfilePath,
  363. FolderPath: gitAction.FolderPath,
  364. ImageRepoURL: gitAction.ImageRepoURI,
  365. BuildEnv: cEnv.Container.Env.Normal,
  366. ClusterID: release.ClusterID,
  367. Version: gitAction.Version,
  368. }
  369. err = gaRunner.Cleanup()
  370. if err != nil {
  371. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  372. Code: ErrReleaseReadData,
  373. Errors: []string{"could not remove github action"},
  374. }, w)
  375. }
  376. }
  377. }
  378. }
  379. w.WriteHeader(http.StatusOK)
  380. return
  381. }