release_handler.go 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726
  1. package api
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "net/url"
  7. "strconv"
  8. "strings"
  9. "sync"
  10. "gorm.io/gorm"
  11. semver "github.com/Masterminds/semver/v3"
  12. "github.com/porter-dev/porter/internal/analytics"
  13. "github.com/porter-dev/porter/internal/kubernetes/prometheus"
  14. "github.com/porter-dev/porter/internal/models"
  15. "github.com/porter-dev/porter/internal/templater/parser"
  16. "helm.sh/helm/v3/pkg/chart"
  17. "helm.sh/helm/v3/pkg/release"
  18. v1 "k8s.io/api/core/v1"
  19. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  20. "github.com/go-chi/chi"
  21. "github.com/porter-dev/porter/internal/forms"
  22. "github.com/porter-dev/porter/internal/helm"
  23. "github.com/porter-dev/porter/internal/helm/grapher"
  24. "github.com/porter-dev/porter/internal/helm/loader"
  25. "github.com/porter-dev/porter/internal/integrations/ci/actions"
  26. "github.com/porter-dev/porter/internal/integrations/slack"
  27. "github.com/porter-dev/porter/internal/kubernetes"
  28. "github.com/porter-dev/porter/internal/repository"
  29. "gopkg.in/yaml.v2"
  30. )
  31. // Enumeration of release API error codes, represented as int64
  32. const (
  33. ErrReleaseDecode ErrorCode = iota + 600
  34. ErrReleaseValidateFields
  35. ErrReleaseReadData
  36. ErrReleaseDeploy
  37. )
  38. var (
  39. createEnvSecretConstraint, _ = semver.NewConstraint(" < 0.1.0")
  40. )
  41. // HandleListReleases retrieves a list of releases for a cluster
  42. // with various filter options
  43. func (app *App) HandleListReleases(w http.ResponseWriter, r *http.Request) {
  44. form := &forms.ListReleaseForm{
  45. ReleaseForm: &forms.ReleaseForm{
  46. Form: &helm.Form{
  47. Repo: app.Repo,
  48. DigitalOceanOAuth: app.DOConf,
  49. },
  50. },
  51. ListFilter: &helm.ListFilter{},
  52. }
  53. agent, err := app.getAgentFromQueryParams(
  54. w,
  55. r,
  56. form.ReleaseForm,
  57. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  58. form.PopulateListFromQueryParams,
  59. )
  60. // errors are handled in app.getAgentFromQueryParams
  61. if err != nil {
  62. return
  63. }
  64. releases, err := agent.ListReleases(form.Namespace, form.ListFilter)
  65. var releaseList []*release.Release
  66. // Clean up unused properties, these values are unnecesary to display the frontend rn
  67. for _, r := range releases {
  68. r.Chart.Files = []*chart.File{}
  69. r.Chart.Templates = []*chart.File{}
  70. r.Manifest = ""
  71. r.Chart.Values = nil
  72. r.Info.Notes = ""
  73. releaseList = append(releaseList, r)
  74. }
  75. if err != nil {
  76. app.handleErrorRead(err, ErrReleaseReadData, w)
  77. return
  78. }
  79. if err := json.NewEncoder(w).Encode(releaseList); err != nil {
  80. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  81. return
  82. }
  83. }
  84. // PorterRelease is a helm release with a form attached
  85. type PorterRelease struct {
  86. *release.Release
  87. Form *models.FormYAML `json:"form"`
  88. HasMetrics bool `json:"has_metrics"`
  89. LatestVersion string `json:"latest_version"`
  90. GitActionConfig *models.GitActionConfigExternal `json:"git_action_config"`
  91. ImageRepoURI string `json:"image_repo_uri"`
  92. }
  93. // HandleGetRelease retrieves a single release based on a name and revision
  94. func (app *App) HandleGetRelease(w http.ResponseWriter, r *http.Request) {
  95. name := chi.URLParam(r, "name")
  96. revision, err := strconv.ParseUint(chi.URLParam(r, "revision"), 0, 64)
  97. form := &forms.GetReleaseForm{
  98. ReleaseForm: &forms.ReleaseForm{
  99. Form: &helm.Form{
  100. Repo: app.Repo,
  101. DigitalOceanOAuth: app.DOConf,
  102. },
  103. },
  104. Name: name,
  105. Revision: int(revision),
  106. }
  107. agent, err := app.getAgentFromQueryParams(
  108. w,
  109. r,
  110. form.ReleaseForm,
  111. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  112. )
  113. // errors are handled in app.getAgentFromQueryParams
  114. if err != nil {
  115. return
  116. }
  117. release, err := agent.GetRelease(form.Name, form.Revision, false)
  118. if err != nil {
  119. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  120. Code: ErrReleaseReadData,
  121. Errors: []string{"release not found"},
  122. }, w)
  123. return
  124. }
  125. // get the filter options
  126. k8sForm := &forms.K8sForm{
  127. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  128. Repo: app.Repo,
  129. DigitalOceanOAuth: app.DOConf,
  130. },
  131. }
  132. vals, err := url.ParseQuery(r.URL.RawQuery)
  133. if err != nil {
  134. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  135. return
  136. }
  137. k8sForm.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
  138. k8sForm.DefaultNamespace = form.ReleaseForm.Namespace
  139. // validate the form
  140. if err := app.validator.Struct(k8sForm); err != nil {
  141. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  142. return
  143. }
  144. // create a new dynamic client
  145. dynClient, err := kubernetes.GetDynamicClientOutOfClusterConfig(k8sForm.OutOfClusterConfig)
  146. if err != nil {
  147. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  148. return
  149. }
  150. parserDef := &parser.ClientConfigDefault{
  151. DynamicClient: dynClient,
  152. HelmChart: release.Chart,
  153. HelmRelease: release,
  154. }
  155. res := &PorterRelease{release, nil, false, "", nil, ""}
  156. for _, file := range release.Chart.Files {
  157. if strings.Contains(file.Name, "form.yaml") {
  158. formYAML, err := parser.FormYAMLFromBytes(parserDef, file.Data, "")
  159. if err != nil {
  160. break
  161. }
  162. res.Form = formYAML
  163. break
  164. }
  165. }
  166. // if form not populated, detect common charts
  167. if res.Form == nil {
  168. // for now just case by name
  169. if res.Release.Chart.Name() == "velero" {
  170. formYAML, err := parser.FormYAMLFromBytes(parserDef, []byte(veleroForm), "")
  171. if err == nil {
  172. res.Form = formYAML
  173. }
  174. }
  175. }
  176. // get prometheus service
  177. _, found, err := prometheus.GetPrometheusService(agent.K8sAgent.Clientset)
  178. if err != nil {
  179. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  180. return
  181. }
  182. res.HasMetrics = found
  183. // detect if Porter application chart and attempt to get the latest version
  184. // from chart repo
  185. chartRepoURL, firstFound := app.ChartLookupURLs[res.Chart.Metadata.Name]
  186. if !firstFound {
  187. app.updateChartRepoURLs()
  188. chartRepoURL, _ = app.ChartLookupURLs[res.Chart.Metadata.Name]
  189. }
  190. if chartRepoURL != "" {
  191. repoIndex, err := loader.LoadRepoIndexPublic(chartRepoURL)
  192. if err == nil {
  193. porterChart := loader.FindPorterChartInIndexList(repoIndex, res.Chart.Metadata.Name)
  194. res.LatestVersion = res.Chart.Metadata.Version
  195. // set latest version to the greater of porterChart.Versions and res.Chart.Metadata.Version
  196. porterChartVersion, porterChartErr := semver.NewVersion(porterChart.Versions[0])
  197. currChartVersion, currChartErr := semver.NewVersion(res.Chart.Metadata.Version)
  198. if currChartErr == nil && porterChartErr == nil && porterChartVersion.GreaterThan(currChartVersion) {
  199. res.LatestVersion = porterChart.Versions[0]
  200. }
  201. }
  202. }
  203. // if the release was created from this server,
  204. modelRelease, err := app.Repo.Release.ReadRelease(form.Cluster.ID, release.Name, release.Namespace)
  205. if modelRelease != nil {
  206. res.ImageRepoURI = modelRelease.ImageRepoURI
  207. gitAction := modelRelease.GitActionConfig
  208. if gitAction.ID != 0 {
  209. res.GitActionConfig = gitAction.Externalize()
  210. }
  211. }
  212. if err := json.NewEncoder(w).Encode(res); err != nil {
  213. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  214. return
  215. }
  216. }
  217. // HandleGetReleaseComponents retrieves kubernetes objects listed in a release identified by name and revision
  218. func (app *App) HandleGetReleaseComponents(w http.ResponseWriter, r *http.Request) {
  219. name := chi.URLParam(r, "name")
  220. revision, err := strconv.ParseUint(chi.URLParam(r, "revision"), 0, 64)
  221. form := &forms.GetReleaseForm{
  222. ReleaseForm: &forms.ReleaseForm{
  223. Form: &helm.Form{
  224. Repo: app.Repo,
  225. DigitalOceanOAuth: app.DOConf,
  226. },
  227. },
  228. Name: name,
  229. Revision: int(revision),
  230. }
  231. agent, err := app.getAgentFromQueryParams(
  232. w,
  233. r,
  234. form.ReleaseForm,
  235. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  236. )
  237. // errors are handled in app.getAgentFromQueryParams
  238. if err != nil {
  239. return
  240. }
  241. release, err := agent.GetRelease(form.Name, form.Revision, false)
  242. if err != nil {
  243. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  244. Code: ErrReleaseReadData,
  245. Errors: []string{"release not found"},
  246. }, w)
  247. return
  248. }
  249. yamlArr := grapher.ImportMultiDocYAML([]byte(release.Manifest))
  250. objects := grapher.ParseObjs(yamlArr, release.Namespace)
  251. parsed := grapher.ParsedObjs{
  252. Objects: objects,
  253. }
  254. parsed.GetControlRel()
  255. parsed.GetLabelRel()
  256. parsed.GetSpecRel()
  257. if err := json.NewEncoder(w).Encode(parsed); err != nil {
  258. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  259. return
  260. }
  261. }
  262. // HandleGetReleaseControllers retrieves controllers that belong to a release.
  263. // Used to display status of charts.
  264. func (app *App) HandleGetReleaseControllers(w http.ResponseWriter, r *http.Request) {
  265. name := chi.URLParam(r, "name")
  266. revision, err := strconv.ParseUint(chi.URLParam(r, "revision"), 0, 64)
  267. form := &forms.GetReleaseForm{
  268. ReleaseForm: &forms.ReleaseForm{
  269. Form: &helm.Form{
  270. Repo: app.Repo,
  271. DigitalOceanOAuth: app.DOConf,
  272. },
  273. },
  274. Name: name,
  275. Revision: int(revision),
  276. }
  277. agent, err := app.getAgentFromQueryParams(
  278. w,
  279. r,
  280. form.ReleaseForm,
  281. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  282. )
  283. // errors are handled in app.getAgentFromQueryParams
  284. if err != nil {
  285. return
  286. }
  287. release, err := agent.GetRelease(form.Name, form.Revision, false)
  288. if err != nil {
  289. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  290. Code: ErrReleaseReadData,
  291. Errors: []string{"release not found"},
  292. }, w)
  293. return
  294. }
  295. vals, err := url.ParseQuery(r.URL.RawQuery)
  296. if err != nil {
  297. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  298. return
  299. }
  300. // get the filter options
  301. k8sForm := &forms.K8sForm{
  302. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  303. Repo: app.Repo,
  304. DigitalOceanOAuth: app.DOConf,
  305. },
  306. }
  307. k8sForm.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
  308. k8sForm.DefaultNamespace = form.ReleaseForm.Namespace
  309. // validate the form
  310. if err := app.validator.Struct(k8sForm); err != nil {
  311. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  312. return
  313. }
  314. // create a new kubernetes agent
  315. var k8sAgent *kubernetes.Agent
  316. if app.ServerConf.IsTesting {
  317. k8sAgent = app.TestAgents.K8sAgent
  318. } else {
  319. k8sAgent, err = kubernetes.GetAgentOutOfClusterConfig(k8sForm.OutOfClusterConfig)
  320. }
  321. yamlArr := grapher.ImportMultiDocYAML([]byte(release.Manifest))
  322. controllers := grapher.ParseControllers(yamlArr)
  323. retrievedControllers := []interface{}{}
  324. // get current status of each controller
  325. // TODO: refactor with type assertion
  326. for _, c := range controllers {
  327. c.Namespace = form.ReleaseForm.Form.Namespace
  328. switch c.Kind {
  329. case "Deployment":
  330. rc, err := k8sAgent.GetDeployment(c)
  331. if err != nil {
  332. app.handleErrorDataRead(err, w)
  333. return
  334. }
  335. rc.Kind = c.Kind
  336. retrievedControllers = append(retrievedControllers, rc)
  337. case "StatefulSet":
  338. rc, err := k8sAgent.GetStatefulSet(c)
  339. if err != nil {
  340. app.handleErrorDataRead(err, w)
  341. return
  342. }
  343. rc.Kind = c.Kind
  344. retrievedControllers = append(retrievedControllers, rc)
  345. case "DaemonSet":
  346. rc, err := k8sAgent.GetDaemonSet(c)
  347. if err != nil {
  348. app.handleErrorDataRead(err, w)
  349. return
  350. }
  351. rc.Kind = c.Kind
  352. retrievedControllers = append(retrievedControllers, rc)
  353. case "ReplicaSet":
  354. rc, err := k8sAgent.GetReplicaSet(c)
  355. if err != nil {
  356. app.handleErrorDataRead(err, w)
  357. return
  358. }
  359. rc.Kind = c.Kind
  360. retrievedControllers = append(retrievedControllers, rc)
  361. case "CronJob":
  362. rc, err := k8sAgent.GetCronJob(c)
  363. if err != nil {
  364. app.handleErrorDataRead(err, w)
  365. return
  366. }
  367. rc.Kind = c.Kind
  368. retrievedControllers = append(retrievedControllers, rc)
  369. }
  370. }
  371. if err := json.NewEncoder(w).Encode(retrievedControllers); err != nil {
  372. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  373. return
  374. }
  375. }
  376. // HandleGetReleaseAllPods retrieves all pods that are associated with a given release.
  377. func (app *App) HandleGetReleaseAllPods(w http.ResponseWriter, r *http.Request) {
  378. name := chi.URLParam(r, "name")
  379. revision, err := strconv.ParseUint(chi.URLParam(r, "revision"), 0, 64)
  380. form := &forms.GetReleaseForm{
  381. ReleaseForm: &forms.ReleaseForm{
  382. Form: &helm.Form{
  383. Repo: app.Repo,
  384. DigitalOceanOAuth: app.DOConf,
  385. },
  386. },
  387. Name: name,
  388. Revision: int(revision),
  389. }
  390. agent, err := app.getAgentFromQueryParams(
  391. w,
  392. r,
  393. form.ReleaseForm,
  394. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  395. )
  396. // errors are handled in app.getAgentFromQueryParams
  397. if err != nil {
  398. return
  399. }
  400. release, err := agent.GetRelease(form.Name, form.Revision, false)
  401. if err != nil {
  402. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  403. Code: ErrReleaseReadData,
  404. Errors: []string{"release not found"},
  405. }, w)
  406. return
  407. }
  408. vals, err := url.ParseQuery(r.URL.RawQuery)
  409. if err != nil {
  410. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  411. return
  412. }
  413. // get the filter options
  414. k8sForm := &forms.K8sForm{
  415. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  416. Repo: app.Repo,
  417. DigitalOceanOAuth: app.DOConf,
  418. },
  419. }
  420. k8sForm.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
  421. k8sForm.DefaultNamespace = form.ReleaseForm.Namespace
  422. // validate the form
  423. if err := app.validator.Struct(k8sForm); err != nil {
  424. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  425. return
  426. }
  427. // create a new kubernetes agent
  428. var k8sAgent *kubernetes.Agent
  429. if app.ServerConf.IsTesting {
  430. k8sAgent = app.TestAgents.K8sAgent
  431. } else {
  432. k8sAgent, err = kubernetes.GetAgentOutOfClusterConfig(k8sForm.OutOfClusterConfig)
  433. }
  434. yamlArr := grapher.ImportMultiDocYAML([]byte(release.Manifest))
  435. controllers := grapher.ParseControllers(yamlArr)
  436. pods := make([]v1.Pod, 0)
  437. // get current status of each controller
  438. for _, c := range controllers {
  439. var selector *metav1.LabelSelector
  440. switch c.Kind {
  441. case "Deployment":
  442. rc, err := k8sAgent.GetDeployment(c)
  443. if err != nil {
  444. app.handleErrorDataRead(err, w)
  445. return
  446. }
  447. selector = rc.Spec.Selector
  448. case "StatefulSet":
  449. rc, err := k8sAgent.GetStatefulSet(c)
  450. if err != nil {
  451. app.handleErrorDataRead(err, w)
  452. return
  453. }
  454. selector = rc.Spec.Selector
  455. case "DaemonSet":
  456. rc, err := k8sAgent.GetDaemonSet(c)
  457. if err != nil {
  458. app.handleErrorDataRead(err, w)
  459. return
  460. }
  461. selector = rc.Spec.Selector
  462. case "ReplicaSet":
  463. rc, err := k8sAgent.GetReplicaSet(c)
  464. if err != nil {
  465. app.handleErrorDataRead(err, w)
  466. return
  467. }
  468. selector = rc.Spec.Selector
  469. case "CronJob":
  470. rc, err := k8sAgent.GetCronJob(c)
  471. if err != nil {
  472. app.handleErrorDataRead(err, w)
  473. return
  474. }
  475. selector = rc.Spec.JobTemplate.Spec.Selector
  476. }
  477. selectors := make([]string, 0)
  478. for key, val := range selector.MatchLabels {
  479. selectors = append(selectors, key+"="+val)
  480. }
  481. namespace := vals.Get("namespace")
  482. podList, err := k8sAgent.GetPodsByLabel(strings.Join(selectors, ","), namespace)
  483. if err != nil {
  484. app.handleErrorDataRead(err, w)
  485. return
  486. }
  487. pods = append(pods, podList.Items...)
  488. }
  489. if err := json.NewEncoder(w).Encode(pods); err != nil {
  490. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  491. return
  492. }
  493. }
  494. type GetJobStatusResult struct {
  495. Status string `json:"status,omitempty"`
  496. StartTime *metav1.Time `json:"start_time,omitempty"`
  497. }
  498. // HandleGetJobStatus gets the status for a specific job
  499. func (app *App) HandleGetJobStatus(w http.ResponseWriter, r *http.Request) {
  500. name := chi.URLParam(r, "name")
  501. namespace := chi.URLParam(r, "namespace")
  502. form := &forms.GetReleaseForm{
  503. ReleaseForm: &forms.ReleaseForm{
  504. Form: &helm.Form{
  505. Repo: app.Repo,
  506. DigitalOceanOAuth: app.DOConf,
  507. Storage: "secret",
  508. Namespace: namespace,
  509. },
  510. },
  511. Name: name,
  512. Revision: 0,
  513. }
  514. agent, err := app.getAgentFromQueryParams(
  515. w,
  516. r,
  517. form.ReleaseForm,
  518. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  519. )
  520. // errors are handled in app.getAgentFromQueryParams
  521. if err != nil {
  522. return
  523. }
  524. release, err := agent.GetRelease(form.Name, form.Revision, false)
  525. if err != nil {
  526. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  527. Code: ErrReleaseReadData,
  528. Errors: []string{"release not found"},
  529. }, w)
  530. return
  531. }
  532. vals, err := url.ParseQuery(r.URL.RawQuery)
  533. if err != nil {
  534. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  535. return
  536. }
  537. // get the filter options
  538. k8sForm := &forms.K8sForm{
  539. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  540. Repo: app.Repo,
  541. DigitalOceanOAuth: app.DOConf,
  542. },
  543. }
  544. k8sForm.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
  545. k8sForm.DefaultNamespace = form.ReleaseForm.Namespace
  546. // validate the form
  547. if err := app.validator.Struct(k8sForm); err != nil {
  548. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  549. return
  550. }
  551. // create a new kubernetes agent
  552. var k8sAgent *kubernetes.Agent
  553. if app.ServerConf.IsTesting {
  554. k8sAgent = app.TestAgents.K8sAgent
  555. } else {
  556. k8sAgent, err = kubernetes.GetAgentOutOfClusterConfig(k8sForm.OutOfClusterConfig)
  557. }
  558. jobs, err := k8sAgent.ListJobsByLabel(namespace, kubernetes.Label{
  559. Key: "helm.sh/chart",
  560. Val: fmt.Sprintf("%s-%s", release.Chart.Name(), release.Chart.Metadata.Version),
  561. }, kubernetes.Label{
  562. Key: "meta.helm.sh/release-name",
  563. Val: name,
  564. })
  565. if err != nil {
  566. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  567. return
  568. }
  569. res := &GetJobStatusResult{}
  570. // get the most recent job
  571. if len(jobs) > 0 {
  572. mostRecentJob := jobs[0]
  573. for _, job := range jobs {
  574. createdAt := job.ObjectMeta.CreationTimestamp
  575. if mostRecentJob.CreationTimestamp.Before(&createdAt) {
  576. mostRecentJob = job
  577. }
  578. }
  579. res.StartTime = mostRecentJob.Status.StartTime
  580. // get the status of the most recent job
  581. if mostRecentJob.Status.Succeeded >= 1 {
  582. res.Status = "succeeded"
  583. } else if mostRecentJob.Status.Active >= 1 {
  584. res.Status = "running"
  585. } else if mostRecentJob.Status.Failed >= 1 {
  586. res.Status = "failed"
  587. }
  588. }
  589. if err := json.NewEncoder(w).Encode(res); err != nil {
  590. app.handleErrorFormDecoding(err, ErrK8sDecode, w)
  591. return
  592. }
  593. }
  594. // HandleListReleaseHistory retrieves a history of releases based on a release name
  595. func (app *App) HandleListReleaseHistory(w http.ResponseWriter, r *http.Request) {
  596. name := chi.URLParam(r, "name")
  597. form := &forms.ListReleaseHistoryForm{
  598. ReleaseForm: &forms.ReleaseForm{
  599. Form: &helm.Form{
  600. Repo: app.Repo,
  601. DigitalOceanOAuth: app.DOConf,
  602. },
  603. },
  604. Name: name,
  605. }
  606. agent, err := app.getAgentFromQueryParams(
  607. w,
  608. r,
  609. form.ReleaseForm,
  610. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  611. )
  612. // errors are handled in app.getAgentFromQueryParams
  613. if err != nil {
  614. return
  615. }
  616. release, err := agent.GetReleaseHistory(form.Name)
  617. if err != nil {
  618. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  619. Code: ErrReleaseReadData,
  620. Errors: []string{"release not found"},
  621. }, w)
  622. return
  623. }
  624. if err := json.NewEncoder(w).Encode(release); err != nil {
  625. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  626. return
  627. }
  628. }
  629. // HandleGetReleaseToken retrieves the webhook token of a specific release.
  630. func (app *App) HandleGetReleaseToken(w http.ResponseWriter, r *http.Request) {
  631. name := chi.URLParam(r, "name")
  632. vals, err := url.ParseQuery(r.URL.RawQuery)
  633. namespace := vals["namespace"][0]
  634. clusterID, err := strconv.ParseUint(vals["cluster_id"][0], 10, 64)
  635. if err != nil {
  636. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  637. Code: ErrReleaseReadData,
  638. Errors: []string{"release not found"},
  639. }, w)
  640. return
  641. }
  642. release, err := app.Repo.Release.ReadRelease(uint(clusterID), name, namespace)
  643. if err != nil {
  644. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  645. Code: ErrReleaseReadData,
  646. Errors: []string{"release not found"},
  647. }, w)
  648. return
  649. }
  650. releaseExt := release.Externalize()
  651. if err := json.NewEncoder(w).Encode(releaseExt); err != nil {
  652. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  653. return
  654. }
  655. }
  656. // HandleCreateWebhookToken creates a new webhook token for a release
  657. func (app *App) HandleCreateWebhookToken(w http.ResponseWriter, r *http.Request) {
  658. name := chi.URLParam(r, "name")
  659. vals, err := url.ParseQuery(r.URL.RawQuery)
  660. if err != nil {
  661. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  662. Code: ErrReleaseReadData,
  663. Errors: []string{"release not found"},
  664. }, w)
  665. return
  666. }
  667. // read the release from the target cluster
  668. form := &forms.ReleaseForm{
  669. Form: &helm.Form{
  670. Repo: app.Repo,
  671. DigitalOceanOAuth: app.DOConf,
  672. },
  673. }
  674. form.PopulateHelmOptionsFromQueryParams(
  675. vals,
  676. app.Repo.Cluster,
  677. )
  678. agent, err := app.getAgentFromReleaseForm(
  679. w,
  680. r,
  681. form,
  682. )
  683. if err != nil {
  684. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  685. return
  686. }
  687. rel, err := agent.GetRelease(name, 0, false)
  688. if err != nil {
  689. app.handleErrorDataRead(err, w)
  690. return
  691. }
  692. token, err := repository.GenerateRandomBytes(16)
  693. if err != nil {
  694. app.handleErrorInternal(err, w)
  695. return
  696. }
  697. // create release with webhook token in db
  698. image, ok := rel.Config["image"].(map[string]interface{})
  699. if !ok {
  700. app.handleErrorInternal(fmt.Errorf("Could not find field image in config"), w)
  701. return
  702. }
  703. repository := image["repository"]
  704. repoStr, ok := repository.(string)
  705. if !ok {
  706. app.handleErrorInternal(fmt.Errorf("Could not find field repository in config"), w)
  707. return
  708. }
  709. release := &models.Release{
  710. ClusterID: form.Form.Cluster.ID,
  711. ProjectID: form.Form.Cluster.ProjectID,
  712. Namespace: form.Form.Namespace,
  713. Name: name,
  714. WebhookToken: token,
  715. ImageRepoURI: repoStr,
  716. }
  717. release, err = app.Repo.Release.CreateRelease(release)
  718. if err != nil {
  719. app.handleErrorInternal(err, w)
  720. return
  721. }
  722. releaseExt := release.Externalize()
  723. if err := json.NewEncoder(w).Encode(releaseExt); err != nil {
  724. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  725. return
  726. }
  727. }
  728. type ContainerEnvConfig struct {
  729. Container struct {
  730. Env struct {
  731. Normal map[string]string `yaml:"normal"`
  732. } `yaml:"env"`
  733. } `yaml:"container"`
  734. }
  735. // HandleUpgradeRelease upgrades a release with new values.yaml
  736. func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
  737. projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
  738. if err != nil || projID == 0 {
  739. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  740. return
  741. }
  742. name := chi.URLParam(r, "name")
  743. vals, err := url.ParseQuery(r.URL.RawQuery)
  744. if err != nil {
  745. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  746. return
  747. }
  748. form := &forms.UpgradeReleaseForm{
  749. ReleaseForm: &forms.ReleaseForm{
  750. Form: &helm.Form{
  751. Repo: app.Repo,
  752. DigitalOceanOAuth: app.DOConf,
  753. },
  754. },
  755. Name: name,
  756. }
  757. form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
  758. vals,
  759. app.Repo.Cluster,
  760. )
  761. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  762. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  763. return
  764. }
  765. agent, err := app.getAgentFromReleaseForm(
  766. w,
  767. r,
  768. form.ReleaseForm,
  769. )
  770. // errors are handled in app.getAgentFromBodyParams
  771. if err != nil {
  772. return
  773. }
  774. registries, err := app.Repo.Registry.ListRegistriesByProjectID(uint(projID))
  775. if err != nil {
  776. app.handleErrorDataRead(err, w)
  777. return
  778. }
  779. conf := &helm.UpgradeReleaseConfig{
  780. Name: form.Name,
  781. Cluster: form.ReleaseForm.Cluster,
  782. Repo: *app.Repo,
  783. Registries: registries,
  784. }
  785. // if the chart version is set, load a chart from the repo
  786. if form.ChartVersion != "" {
  787. release, err := agent.GetRelease(form.Name, 0, false)
  788. if err != nil {
  789. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  790. Code: ErrReleaseReadData,
  791. Errors: []string{"chart version not found"},
  792. }, w)
  793. return
  794. }
  795. chartRepoURL, foundFirst := app.ChartLookupURLs[release.Chart.Metadata.Name]
  796. if !foundFirst {
  797. app.updateChartRepoURLs()
  798. var found bool
  799. chartRepoURL, found = app.ChartLookupURLs[release.Chart.Metadata.Name]
  800. if !found {
  801. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  802. Code: ErrReleaseReadData,
  803. Errors: []string{"chart not found"},
  804. }, w)
  805. return
  806. }
  807. }
  808. chart, err := loader.LoadChartPublic(
  809. chartRepoURL,
  810. release.Chart.Metadata.Name,
  811. form.ChartVersion,
  812. )
  813. if err != nil {
  814. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  815. Code: ErrReleaseReadData,
  816. Errors: []string{"chart not found"},
  817. }, w)
  818. return
  819. }
  820. conf.Chart = chart
  821. }
  822. rel, upgradeErr := agent.UpgradeRelease(conf, form.Values, app.DOConf)
  823. slackInts, _ := app.Repo.SlackIntegration.ListSlackIntegrationsByProjectID(uint(projID))
  824. clusterID, err := strconv.ParseUint(vals["cluster_id"][0], 10, 64)
  825. release, _ := app.Repo.Release.ReadRelease(uint(clusterID), name, form.Namespace)
  826. var notifConf *models.NotificationConfigExternal
  827. notifConf = nil
  828. if release != nil && release.NotificationConfig != 0 {
  829. conf, err := app.Repo.NotificationConfig.ReadNotificationConfig(release.NotificationConfig)
  830. if err != nil {
  831. app.handleErrorInternal(err, w)
  832. return
  833. }
  834. notifConf = conf.Externalize()
  835. }
  836. notifier := slack.NewSlackNotifier(notifConf, slackInts...)
  837. notifyOpts := &slack.NotifyOpts{
  838. ProjectID: uint(projID),
  839. ClusterID: form.Cluster.ID,
  840. ClusterName: form.Cluster.Name,
  841. Name: name,
  842. Namespace: form.Namespace,
  843. URL: fmt.Sprintf(
  844. "%s/applications/%s/%s/%s",
  845. app.ServerConf.ServerURL,
  846. url.PathEscape(form.Cluster.Name),
  847. form.Namespace,
  848. name,
  849. ) + fmt.Sprintf("?project_id=%d", uint(projID)),
  850. }
  851. if upgradeErr != nil {
  852. notifyOpts.Status = slack.StatusFailed
  853. notifyOpts.Info = upgradeErr.Error()
  854. notifier.Notify(notifyOpts)
  855. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  856. Code: ErrReleaseDeploy,
  857. Errors: []string{upgradeErr.Error()},
  858. }, w)
  859. return
  860. }
  861. notifyOpts.Status = string(rel.Info.Status)
  862. notifyOpts.Version = rel.Version
  863. notifier.Notify(notifyOpts)
  864. // update the github actions env if the release exists and is built from source
  865. if cName := rel.Chart.Metadata.Name; cName == "job" || cName == "web" || cName == "worker" {
  866. if err != nil {
  867. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  868. Code: ErrReleaseReadData,
  869. Errors: []string{"release not found"},
  870. }, w)
  871. return
  872. }
  873. if release != nil {
  874. // update image repo uri if changed
  875. repository := rel.Config["image"].(map[string]interface{})["repository"]
  876. repoStr, ok := repository.(string)
  877. if !ok {
  878. app.handleErrorInternal(fmt.Errorf("Could not find field repository in config"), w)
  879. return
  880. }
  881. if repoStr != release.ImageRepoURI {
  882. release, err = app.Repo.Release.UpdateRelease(release)
  883. if err != nil {
  884. app.handleErrorInternal(err, w)
  885. return
  886. }
  887. }
  888. gitAction := release.GitActionConfig
  889. if gitAction.ID != 0 {
  890. // parse env into build env
  891. cEnv := &ContainerEnvConfig{}
  892. yaml.Unmarshal([]byte(form.Values), cEnv)
  893. gr, err := app.Repo.GitRepo.ReadGitRepo(gitAction.GitRepoID)
  894. if err != nil {
  895. if err != gorm.ErrRecordNotFound {
  896. app.handleErrorInternal(err, w)
  897. return
  898. }
  899. gr = nil
  900. }
  901. repoSplit := strings.Split(gitAction.GitRepo, "/")
  902. gaRunner := &actions.GithubActions{
  903. ServerURL: app.ServerConf.ServerURL,
  904. GithubOAuthIntegration: gr,
  905. GithubInstallationID: gitAction.GithubInstallationID,
  906. GithubAppID: app.GithubAppConf.AppID,
  907. GithubAppSecretPath: app.GithubAppConf.SecretPath,
  908. GitRepoName: repoSplit[1],
  909. GitRepoOwner: repoSplit[0],
  910. Repo: *app.Repo,
  911. GithubConf: app.GithubProjectConf,
  912. ProjectID: uint(projID),
  913. ReleaseName: name,
  914. ReleaseNamespace: release.Namespace,
  915. GitBranch: gitAction.GitBranch,
  916. DockerFilePath: gitAction.DockerfilePath,
  917. FolderPath: gitAction.FolderPath,
  918. ImageRepoURL: gitAction.ImageRepoURI,
  919. BuildEnv: cEnv.Container.Env.Normal,
  920. ClusterID: release.ClusterID,
  921. Version: gitAction.Version,
  922. }
  923. actionVersion, err := semver.NewVersion(gaRunner.Version)
  924. if err != nil {
  925. app.handleErrorInternal(err, w)
  926. }
  927. if createEnvSecretConstraint.Check(actionVersion) {
  928. if err := gaRunner.CreateEnvSecret(); err != nil {
  929. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  930. Code: ErrReleaseReadData,
  931. Errors: []string{"could not update github secret"},
  932. }, w)
  933. }
  934. }
  935. }
  936. }
  937. }
  938. w.WriteHeader(http.StatusOK)
  939. }
  940. // HandleReleaseDeployWebhook upgrades a release when a chart specific webhook is called.
  941. func (app *App) HandleReleaseDeployWebhook(w http.ResponseWriter, r *http.Request) {
  942. token := chi.URLParam(r, "token")
  943. // retrieve release by token
  944. release, err := app.Repo.Release.ReadReleaseByWebhookToken(token)
  945. if err != nil {
  946. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  947. Code: ErrReleaseReadData,
  948. Errors: []string{"release not found with given webhook"},
  949. }, w)
  950. return
  951. }
  952. params := map[string][]string{}
  953. params["cluster_id"] = []string{fmt.Sprint(release.ClusterID)}
  954. params["storage"] = []string{"secret"}
  955. params["namespace"] = []string{release.Namespace}
  956. vals, err := url.ParseQuery(r.URL.RawQuery)
  957. if err != nil {
  958. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  959. return
  960. }
  961. form := &forms.UpgradeReleaseForm{
  962. ReleaseForm: &forms.ReleaseForm{
  963. Form: &helm.Form{
  964. Repo: app.Repo,
  965. DigitalOceanOAuth: app.DOConf,
  966. },
  967. },
  968. Name: release.Name,
  969. }
  970. form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
  971. params,
  972. app.Repo.Cluster,
  973. )
  974. agent, err := app.getAgentFromReleaseForm(
  975. w,
  976. r,
  977. form.ReleaseForm,
  978. )
  979. // errors are handled in app.getAgentFromBodyParams
  980. if err != nil {
  981. return
  982. }
  983. rel, err := agent.GetRelease(form.Name, 0, false)
  984. // repository is set to current repository by default
  985. commit := vals["commit"][0]
  986. repository := rel.Config["image"].(map[string]interface{})["repository"]
  987. gitAction := release.GitActionConfig
  988. if gitAction.ID != 0 && (repository == "porterdev/hello-porter" || repository == "public.ecr.aws/o1j4x7p4/hello-porter") {
  989. repository = gitAction.ImageRepoURI
  990. } else if gitAction.ID != 0 && (repository == "porterdev/hello-porter-job" || repository == "public.ecr.aws/o1j4x7p4/hello-porter-job") {
  991. repository = gitAction.ImageRepoURI
  992. }
  993. image := map[string]interface{}{}
  994. image["repository"] = repository
  995. image["tag"] = commit
  996. rel.Config["image"] = image
  997. if rel.Config["auto_deploy"] == false {
  998. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  999. Code: ErrReleaseDeploy,
  1000. Errors: []string{"Deploy webhook is disabled for this deployment."},
  1001. }, w)
  1002. return
  1003. }
  1004. registries, err := app.Repo.Registry.ListRegistriesByProjectID(uint(form.ReleaseForm.Cluster.ProjectID))
  1005. if err != nil {
  1006. app.handleErrorDataRead(err, w)
  1007. return
  1008. }
  1009. conf := &helm.UpgradeReleaseConfig{
  1010. Name: form.Name,
  1011. Cluster: form.ReleaseForm.Cluster,
  1012. Repo: *app.Repo,
  1013. Registries: registries,
  1014. Values: rel.Config,
  1015. }
  1016. slackInts, _ := app.Repo.SlackIntegration.ListSlackIntegrationsByProjectID(uint(form.ReleaseForm.Cluster.ProjectID))
  1017. var notifConf *models.NotificationConfigExternal
  1018. notifConf = nil
  1019. if release != nil && release.NotificationConfig != 0 {
  1020. conf, err := app.Repo.NotificationConfig.ReadNotificationConfig(release.NotificationConfig)
  1021. if err != nil {
  1022. app.handleErrorInternal(err, w)
  1023. return
  1024. }
  1025. notifConf = conf.Externalize()
  1026. }
  1027. notifier := slack.NewSlackNotifier(notifConf, slackInts...)
  1028. notifyOpts := &slack.NotifyOpts{
  1029. ProjectID: uint(form.ReleaseForm.Cluster.ProjectID),
  1030. ClusterID: form.Cluster.ID,
  1031. ClusterName: form.Cluster.Name,
  1032. Name: rel.Name,
  1033. Namespace: rel.Namespace,
  1034. URL: fmt.Sprintf(
  1035. "%s/applications/%s/%s/%s",
  1036. app.ServerConf.ServerURL,
  1037. url.PathEscape(form.Cluster.Name),
  1038. form.Namespace,
  1039. rel.Name,
  1040. ) + fmt.Sprintf("?project_id=%d", uint(form.ReleaseForm.Cluster.ProjectID)),
  1041. }
  1042. rel, err = agent.UpgradeReleaseByValues(conf, app.DOConf)
  1043. if err != nil {
  1044. notifyOpts.Status = slack.StatusFailed
  1045. notifyOpts.Info = err.Error()
  1046. notifier.Notify(notifyOpts)
  1047. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  1048. Code: ErrReleaseDeploy,
  1049. Errors: []string{err.Error()},
  1050. }, w)
  1051. return
  1052. }
  1053. notifyOpts.Status = string(rel.Info.Status)
  1054. notifyOpts.Version = rel.Version
  1055. notifier.Notify(notifyOpts)
  1056. userID, _ := app.getUserIDFromRequest(r)
  1057. app.AnalyticsClient.Track(analytics.ApplicationDeploymentWebhookTrack(&analytics.ApplicationDeploymentWebhookTrackOpts{
  1058. ImageURI: fmt.Sprintf("%v", repository),
  1059. ApplicationScopedTrackOpts: analytics.GetApplicationScopedTrackOpts(
  1060. userID,
  1061. release.ProjectID,
  1062. release.ClusterID,
  1063. release.Name,
  1064. release.Namespace,
  1065. rel.Chart.Metadata.Name,
  1066. ),
  1067. }))
  1068. w.WriteHeader(http.StatusOK)
  1069. }
  1070. // HandleReleaseJobUpdateImage
  1071. func (app *App) HandleReleaseUpdateJobImages(w http.ResponseWriter, r *http.Request) {
  1072. vals, err := url.ParseQuery(r.URL.RawQuery)
  1073. if err != nil {
  1074. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  1075. return
  1076. }
  1077. form := &forms.UpdateImageForm{
  1078. ReleaseForm: &forms.ReleaseForm{
  1079. Form: &helm.Form{
  1080. Repo: app.Repo,
  1081. DigitalOceanOAuth: app.DOConf,
  1082. },
  1083. },
  1084. }
  1085. form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
  1086. vals,
  1087. app.Repo.Cluster,
  1088. )
  1089. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  1090. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  1091. return
  1092. }
  1093. releases, err := app.Repo.Release.ListReleasesByImageRepoURI(form.Cluster.ID, form.ImageRepoURI)
  1094. if err != nil {
  1095. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  1096. Code: ErrReleaseReadData,
  1097. Errors: []string{"releases not found with given image repo uri"},
  1098. }, w)
  1099. return
  1100. }
  1101. agent, err := app.getAgentFromReleaseForm(
  1102. w,
  1103. r,
  1104. form.ReleaseForm,
  1105. )
  1106. // errors are handled in app.getAgentFromBodyParams
  1107. if err != nil {
  1108. return
  1109. }
  1110. registries, err := app.Repo.Registry.ListRegistriesByProjectID(uint(form.ReleaseForm.Cluster.ProjectID))
  1111. if err != nil {
  1112. app.handleErrorDataRead(err, w)
  1113. return
  1114. }
  1115. // asynchronously update releases with that image repo uri
  1116. var wg sync.WaitGroup
  1117. mu := &sync.Mutex{}
  1118. errors := make([]string, 0)
  1119. for i := range releases {
  1120. index := i
  1121. wg.Add(1)
  1122. go func() {
  1123. defer wg.Done()
  1124. // read release via agent
  1125. rel, err := agent.GetRelease(releases[index].Name, 0, false)
  1126. if err != nil {
  1127. mu.Lock()
  1128. errors = append(errors, err.Error())
  1129. mu.Unlock()
  1130. }
  1131. if rel.Chart.Name() == "job" {
  1132. image := map[string]interface{}{}
  1133. image["repository"] = releases[index].ImageRepoURI
  1134. image["tag"] = form.Tag
  1135. rel.Config["image"] = image
  1136. rel.Config["paused"] = true
  1137. conf := &helm.UpgradeReleaseConfig{
  1138. Name: releases[index].Name,
  1139. Cluster: form.ReleaseForm.Cluster,
  1140. Repo: *app.Repo,
  1141. Registries: registries,
  1142. Values: rel.Config,
  1143. }
  1144. _, err = agent.UpgradeReleaseByValues(conf, app.DOConf)
  1145. if err != nil {
  1146. mu.Lock()
  1147. errors = append(errors, err.Error())
  1148. mu.Unlock()
  1149. }
  1150. }
  1151. }()
  1152. }
  1153. wg.Wait()
  1154. w.WriteHeader(http.StatusOK)
  1155. }
  1156. // HandleRollbackRelease rolls a release back to a specified revision
  1157. func (app *App) HandleRollbackRelease(w http.ResponseWriter, r *http.Request) {
  1158. name := chi.URLParam(r, "name")
  1159. vals, err := url.ParseQuery(r.URL.RawQuery)
  1160. if err != nil {
  1161. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  1162. return
  1163. }
  1164. form := &forms.RollbackReleaseForm{
  1165. ReleaseForm: &forms.ReleaseForm{
  1166. Form: &helm.Form{
  1167. Repo: app.Repo,
  1168. DigitalOceanOAuth: app.DOConf,
  1169. },
  1170. },
  1171. Name: name,
  1172. }
  1173. form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
  1174. vals,
  1175. app.Repo.Cluster,
  1176. )
  1177. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  1178. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  1179. return
  1180. }
  1181. agent, err := app.getAgentFromReleaseForm(
  1182. w,
  1183. r,
  1184. form.ReleaseForm,
  1185. )
  1186. // errors are handled in app.getAgentFromBodyParams
  1187. if err != nil {
  1188. return
  1189. }
  1190. err = agent.RollbackRelease(form.Name, form.Revision)
  1191. if err != nil {
  1192. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  1193. Code: ErrReleaseDeploy,
  1194. Errors: []string{"error rolling back release " + err.Error()},
  1195. }, w)
  1196. return
  1197. }
  1198. // get the full release data for GHA updating
  1199. rel, err := agent.GetRelease(form.Name, form.Revision, false)
  1200. if err != nil {
  1201. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  1202. Code: ErrReleaseReadData,
  1203. Errors: []string{"release not found"},
  1204. }, w)
  1205. return
  1206. }
  1207. // update the github actions env if the release exists and is built from source
  1208. if cName := rel.Chart.Metadata.Name; cName == "job" || cName == "web" || cName == "worker" {
  1209. clusterID, err := strconv.ParseUint(vals["cluster_id"][0], 10, 64)
  1210. if err != nil {
  1211. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  1212. Code: ErrReleaseReadData,
  1213. Errors: []string{"release not found"},
  1214. }, w)
  1215. return
  1216. }
  1217. release, err := app.Repo.Release.ReadRelease(uint(clusterID), name, rel.Namespace)
  1218. if release != nil {
  1219. // update image repo uri if changed
  1220. repository := rel.Config["image"].(map[string]interface{})["repository"]
  1221. repoStr, ok := repository.(string)
  1222. if !ok {
  1223. app.handleErrorInternal(fmt.Errorf("Could not find field repository in config"), w)
  1224. return
  1225. }
  1226. if repoStr != release.ImageRepoURI {
  1227. release, err = app.Repo.Release.UpdateRelease(release)
  1228. if err != nil {
  1229. app.handleErrorInternal(err, w)
  1230. return
  1231. }
  1232. }
  1233. gitAction := release.GitActionConfig
  1234. if gitAction.ID != 0 {
  1235. // parse env into build env
  1236. cEnv := &ContainerEnvConfig{}
  1237. rawValues, err := yaml.Marshal(rel.Config)
  1238. if err != nil {
  1239. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  1240. Code: ErrReleaseReadData,
  1241. Errors: []string{"could not get values of previous revision"},
  1242. }, w)
  1243. }
  1244. yaml.Unmarshal(rawValues, cEnv)
  1245. gr, err := app.Repo.GitRepo.ReadGitRepo(gitAction.GitRepoID)
  1246. if err != nil {
  1247. if err != gorm.ErrRecordNotFound {
  1248. app.handleErrorInternal(err, w)
  1249. return
  1250. }
  1251. gr = nil
  1252. }
  1253. repoSplit := strings.Split(gitAction.GitRepo, "/")
  1254. projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
  1255. if err != nil || projID == 0 {
  1256. app.handleErrorFormDecoding(err, ErrProjectDecode, w)
  1257. return
  1258. }
  1259. gaRunner := &actions.GithubActions{
  1260. ServerURL: app.ServerConf.ServerURL,
  1261. GithubOAuthIntegration: gr,
  1262. GithubInstallationID: gitAction.GithubInstallationID,
  1263. GithubAppID: app.GithubAppConf.AppID,
  1264. GithubAppSecretPath: app.GithubAppConf.SecretPath,
  1265. GitRepoName: repoSplit[1],
  1266. GitRepoOwner: repoSplit[0],
  1267. Repo: *app.Repo,
  1268. GithubConf: app.GithubProjectConf,
  1269. ProjectID: uint(projID),
  1270. ReleaseName: name,
  1271. ReleaseNamespace: release.Namespace,
  1272. GitBranch: gitAction.GitBranch,
  1273. DockerFilePath: gitAction.DockerfilePath,
  1274. FolderPath: gitAction.FolderPath,
  1275. ImageRepoURL: gitAction.ImageRepoURI,
  1276. BuildEnv: cEnv.Container.Env.Normal,
  1277. ClusterID: release.ClusterID,
  1278. Version: gitAction.Version,
  1279. }
  1280. actionVersion, err := semver.NewVersion(gaRunner.Version)
  1281. if err != nil {
  1282. app.handleErrorInternal(err, w)
  1283. }
  1284. if createEnvSecretConstraint.Check(actionVersion) {
  1285. if err := gaRunner.CreateEnvSecret(); err != nil {
  1286. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  1287. Code: ErrReleaseReadData,
  1288. Errors: []string{"could not update github secret"},
  1289. }, w)
  1290. }
  1291. }
  1292. }
  1293. }
  1294. }
  1295. w.WriteHeader(http.StatusOK)
  1296. }
  1297. // ------------------------ Release handler helper functions ------------------------ //
  1298. // getAgentFromQueryParams uses the query params to populate a form, and then
  1299. // passes that form to the underlying app.getAgentFromReleaseForm to create a new
  1300. // Helm agent.
  1301. func (app *App) getAgentFromQueryParams(
  1302. w http.ResponseWriter,
  1303. r *http.Request,
  1304. form *forms.ReleaseForm,
  1305. // populate uses the query params to populate a form
  1306. populate ...func(vals url.Values, repo repository.ClusterRepository) error,
  1307. ) (*helm.Agent, error) {
  1308. vals, err := url.ParseQuery(r.URL.RawQuery)
  1309. if err != nil {
  1310. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  1311. return nil, err
  1312. }
  1313. for _, f := range populate {
  1314. err := f(vals, app.Repo.Cluster)
  1315. if err != nil {
  1316. app.handleErrorInternal(err, w)
  1317. return nil, err
  1318. }
  1319. }
  1320. return app.getAgentFromReleaseForm(w, r, form)
  1321. }
  1322. // getAgentFromReleaseForm uses a non-validated form to construct a new Helm agent based on
  1323. // the userID found in the session and the options required by the Helm agent.
  1324. func (app *App) getAgentFromReleaseForm(
  1325. w http.ResponseWriter,
  1326. r *http.Request,
  1327. form *forms.ReleaseForm,
  1328. ) (*helm.Agent, error) {
  1329. var err error
  1330. // validate the form
  1331. if err := app.validator.Struct(form); err != nil {
  1332. app.handleErrorFormValidation(err, ErrReleaseValidateFields, w)
  1333. return nil, err
  1334. }
  1335. // create a new agent
  1336. var agent *helm.Agent
  1337. if app.ServerConf.IsTesting {
  1338. agent = app.TestAgents.HelmAgent
  1339. } else {
  1340. agent, err = helm.GetAgentOutOfClusterConfig(form.Form, app.Logger)
  1341. }
  1342. if err != nil {
  1343. app.handleErrorInternal(err, w)
  1344. }
  1345. return agent, err
  1346. }
  1347. const veleroForm string = `tags:
  1348. - hello
  1349. tabs:
  1350. - name: main
  1351. context:
  1352. type: cluster
  1353. config:
  1354. group: velero.io
  1355. version: v1
  1356. resource: backups
  1357. label: Backups
  1358. sections:
  1359. - name: section_one
  1360. contents:
  1361. - type: heading
  1362. label: 💾 Velero Backups
  1363. - type: resource-list
  1364. value: |
  1365. .items[] | {
  1366. name: .metadata.name,
  1367. label: .metadata.namespace,
  1368. status: .status.phase,
  1369. timestamp: .status.completionTimestamp,
  1370. message: [
  1371. (if .status.volumeSnapshotsAttempted then "\(.status.volumeSnapshotsAttempted) volume snapshots attempted, \(.status.volumeSnapshotsCompleted) completed." else null end),
  1372. "Finished \(.status.completionTimestamp).",
  1373. "Backup expires on \(.status.expiration)."
  1374. ]|join(" "),
  1375. data: {
  1376. "Included Namespaces": (if .spec.includedNamespaces then .spec.includedNamespaces|join(",") else "* (all)" end),
  1377. "Included Resources": (if .spec.includedResources then .spec.includedResources|join(",") else "* (all)" end),
  1378. "Storage Location": .spec.storageLocation
  1379. }
  1380. }`