release_handler.go 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844
  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. // HandleGetReleaseSteps returns a list of all steps for a given release
  1298. // note that steps are not guaranteed to be in any specific order, so they should be ordered if needed
  1299. func (app *App) HandleGetReleaseSteps(w http.ResponseWriter, r *http.Request) {
  1300. name := chi.URLParam(r, "name")
  1301. vals, err := url.ParseQuery(r.URL.RawQuery)
  1302. if err != nil {
  1303. app.handleErrorInternal(err, w)
  1304. }
  1305. namespace := vals["namespace"][0]
  1306. clusterId, err := strconv.ParseUint(vals["cluster_id"][0], 0, 64)
  1307. if err != nil {
  1308. app.handleErrorInternal(err, w)
  1309. return
  1310. }
  1311. rel, err := app.Repo.Release.ReadRelease(uint(clusterId), name, namespace)
  1312. if err != nil {
  1313. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  1314. Code: ErrReleaseReadData,
  1315. Errors: []string{"release not found"},
  1316. }, w)
  1317. return
  1318. }
  1319. res := make([]models.SubEventExternal, 0)
  1320. if rel.EventContainer != 0 {
  1321. subevents, err := app.Repo.Event.ReadEventsByContainerID(rel.EventContainer)
  1322. if err != nil {
  1323. app.handleErrorInternal(err, w)
  1324. }
  1325. for _, sub := range subevents {
  1326. res = append(res, sub.Externalize())
  1327. }
  1328. }
  1329. json.NewEncoder(w).Encode(res)
  1330. }
  1331. type HandleUpdateReleaseStepsForm struct {
  1332. Event struct {
  1333. ID string `json:"event_id" form:"required"`
  1334. Name string `json:"name" form:"required"`
  1335. Index int64 `json:"index" form:"required"`
  1336. Status models.EventStatus `json:"status" form:"required"`
  1337. Info string `json:"info" form:"required"`
  1338. } `json:"event" form:"required"`
  1339. Name string `json:"name"`
  1340. Namespace string `json:"namespace"`
  1341. ClusterID uint `json:"cluster_id"`
  1342. }
  1343. // HandleUpdateReleaseSteps adds a new step to a release
  1344. func (app *App) HandleUpdateReleaseSteps(w http.ResponseWriter, r *http.Request) {
  1345. form := &HandleUpdateReleaseStepsForm{}
  1346. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  1347. app.handleErrorInternal(err, w)
  1348. return
  1349. }
  1350. rel, err := app.Repo.Release.ReadRelease(form.ClusterID, form.Name, form.Namespace)
  1351. if err != nil {
  1352. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  1353. Code: ErrReleaseReadData,
  1354. Errors: []string{"Release not found"},
  1355. }, w)
  1356. return
  1357. }
  1358. if rel.EventContainer == 0 {
  1359. // create new event container
  1360. container, err := app.Repo.Event.CreateEventContainer(&models.EventContainer{ReleaseID: rel.ID})
  1361. if err != nil {
  1362. app.handleErrorDataWrite(err, w)
  1363. return
  1364. }
  1365. rel.EventContainer = container.ID
  1366. rel, err = app.Repo.Release.UpdateRelease(rel)
  1367. if err != nil {
  1368. app.handleErrorInternal(err, w)
  1369. return
  1370. }
  1371. }
  1372. container, err := app.Repo.Event.ReadEventContainer(rel.EventContainer)
  1373. if err != nil {
  1374. app.handleErrorInternal(err, w)
  1375. return
  1376. }
  1377. if err := app.Repo.Event.AppendEvent(container, &models.SubEvent{
  1378. EventContainerID: container.ID,
  1379. EventID: form.Event.ID,
  1380. Name: form.Event.Name,
  1381. Index: form.Event.Index,
  1382. Status: form.Event.Status,
  1383. Info: form.Event.Info,
  1384. }); err != nil {
  1385. app.handleErrorInternal(err, w)
  1386. }
  1387. }
  1388. // ------------------------ Release handler helper functions ------------------------ //
  1389. // getAgentFromQueryParams uses the query params to populate a form, and then
  1390. // passes that form to the underlying app.getAgentFromReleaseForm to create a new
  1391. // Helm agent.
  1392. func (app *App) getAgentFromQueryParams(
  1393. w http.ResponseWriter,
  1394. r *http.Request,
  1395. form *forms.ReleaseForm,
  1396. // populate uses the query params to populate a form
  1397. populate ...func(vals url.Values, repo repository.ClusterRepository) error,
  1398. ) (*helm.Agent, error) {
  1399. vals, err := url.ParseQuery(r.URL.RawQuery)
  1400. if err != nil {
  1401. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  1402. return nil, err
  1403. }
  1404. for _, f := range populate {
  1405. err := f(vals, app.Repo.Cluster)
  1406. if err != nil {
  1407. app.handleErrorInternal(err, w)
  1408. return nil, err
  1409. }
  1410. }
  1411. return app.getAgentFromReleaseForm(w, r, form)
  1412. }
  1413. // getAgentFromReleaseForm uses a non-validated form to construct a new Helm agent based on
  1414. // the userID found in the session and the options required by the Helm agent.
  1415. func (app *App) getAgentFromReleaseForm(
  1416. w http.ResponseWriter,
  1417. r *http.Request,
  1418. form *forms.ReleaseForm,
  1419. ) (*helm.Agent, error) {
  1420. var err error
  1421. // validate the form
  1422. if err := app.validator.Struct(form); err != nil {
  1423. app.handleErrorFormValidation(err, ErrReleaseValidateFields, w)
  1424. return nil, err
  1425. }
  1426. // create a new agent
  1427. var agent *helm.Agent
  1428. if app.ServerConf.IsTesting {
  1429. agent = app.TestAgents.HelmAgent
  1430. } else {
  1431. agent, err = helm.GetAgentOutOfClusterConfig(form.Form, app.Logger)
  1432. }
  1433. if err != nil {
  1434. app.handleErrorInternal(err, w)
  1435. }
  1436. return agent, err
  1437. }
  1438. const veleroForm string = `tags:
  1439. - hello
  1440. tabs:
  1441. - name: main
  1442. context:
  1443. type: cluster
  1444. config:
  1445. group: velero.io
  1446. version: v1
  1447. resource: backups
  1448. label: Backups
  1449. sections:
  1450. - name: section_one
  1451. contents:
  1452. - type: heading
  1453. label: 💾 Velero Backups
  1454. - type: resource-list
  1455. value: |
  1456. .items[] | {
  1457. name: .metadata.name,
  1458. label: .metadata.namespace,
  1459. status: .status.phase,
  1460. timestamp: .status.completionTimestamp,
  1461. message: [
  1462. (if .status.volumeSnapshotsAttempted then "\(.status.volumeSnapshotsAttempted) volume snapshots attempted, \(.status.volumeSnapshotsCompleted) completed." else null end),
  1463. "Finished \(.status.completionTimestamp).",
  1464. "Backup expires on \(.status.expiration)."
  1465. ]|join(" "),
  1466. data: {
  1467. "Included Namespaces": (if .spec.includedNamespaces then .spec.includedNamespaces|join(",") else "* (all)" end),
  1468. "Included Resources": (if .spec.includedResources then .spec.includedResources|join(",") else "* (all)" end),
  1469. "Storage Location": .spec.storageLocation
  1470. }
  1471. }`