release_handler.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  1. package api
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "net/url"
  7. "strconv"
  8. "strings"
  9. "github.com/porter-dev/porter/internal/models"
  10. "github.com/porter-dev/porter/internal/templater/parser"
  11. "helm.sh/helm/v3/pkg/release"
  12. "github.com/go-chi/chi"
  13. "github.com/porter-dev/porter/internal/forms"
  14. "github.com/porter-dev/porter/internal/helm"
  15. "github.com/porter-dev/porter/internal/helm/grapher"
  16. "github.com/porter-dev/porter/internal/kubernetes"
  17. "github.com/porter-dev/porter/internal/repository"
  18. )
  19. // Enumeration of release API error codes, represented as int64
  20. const (
  21. ErrReleaseDecode ErrorCode = iota + 600
  22. ErrReleaseValidateFields
  23. ErrReleaseReadData
  24. ErrReleaseDeploy
  25. )
  26. // HandleListReleases retrieves a list of releases for a cluster
  27. // with various filter options
  28. func (app *App) HandleListReleases(w http.ResponseWriter, r *http.Request) {
  29. form := &forms.ListReleaseForm{
  30. ReleaseForm: &forms.ReleaseForm{
  31. Form: &helm.Form{
  32. Repo: app.Repo,
  33. },
  34. },
  35. ListFilter: &helm.ListFilter{},
  36. }
  37. agent, err := app.getAgentFromQueryParams(
  38. w,
  39. r,
  40. form.ReleaseForm,
  41. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  42. form.PopulateListFromQueryParams,
  43. )
  44. // errors are handled in app.getAgentFromQueryParams
  45. if err != nil {
  46. return
  47. }
  48. releases, err := agent.ListReleases(form.Namespace, form.ListFilter)
  49. if err != nil {
  50. app.handleErrorRead(err, ErrReleaseReadData, w)
  51. return
  52. }
  53. if err := json.NewEncoder(w).Encode(releases); err != nil {
  54. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  55. return
  56. }
  57. }
  58. // PorterRelease is a helm release with a form attached
  59. type PorterRelease struct {
  60. *release.Release
  61. Form *models.FormYAML `json:"form"`
  62. }
  63. // HandleGetRelease retrieves a single release based on a name and revision
  64. func (app *App) HandleGetRelease(w http.ResponseWriter, r *http.Request) {
  65. name := chi.URLParam(r, "name")
  66. revision, err := strconv.ParseUint(chi.URLParam(r, "revision"), 0, 64)
  67. form := &forms.GetReleaseForm{
  68. ReleaseForm: &forms.ReleaseForm{
  69. Form: &helm.Form{
  70. Repo: app.Repo,
  71. },
  72. },
  73. Name: name,
  74. Revision: int(revision),
  75. }
  76. agent, err := app.getAgentFromQueryParams(
  77. w,
  78. r,
  79. form.ReleaseForm,
  80. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  81. )
  82. // errors are handled in app.getAgentFromQueryParams
  83. if err != nil {
  84. return
  85. }
  86. release, err := agent.GetRelease(form.Name, form.Revision)
  87. if err != nil {
  88. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  89. Code: ErrReleaseReadData,
  90. Errors: []string{"release not found"},
  91. }, w)
  92. return
  93. }
  94. // get the filter options
  95. k8sForm := &forms.K8sForm{
  96. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  97. Repo: app.Repo,
  98. },
  99. }
  100. vals, err := url.ParseQuery(r.URL.RawQuery)
  101. if err != nil {
  102. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  103. return
  104. }
  105. k8sForm.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
  106. // validate the form
  107. if err := app.validator.Struct(k8sForm); err != nil {
  108. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  109. return
  110. }
  111. // create a new dynamic client
  112. dynClient, err := kubernetes.GetDynamicClientOutOfClusterConfig(k8sForm.OutOfClusterConfig)
  113. if err != nil {
  114. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  115. return
  116. }
  117. parserDef := &parser.ClientConfigDefault{
  118. DynamicClient: dynClient,
  119. HelmChart: release.Chart,
  120. HelmRelease: release,
  121. }
  122. res := &PorterRelease{release, nil}
  123. for _, file := range release.Chart.Files {
  124. if strings.Contains(file.Name, "form.yaml") {
  125. formYAML, err := parser.FormYAMLFromBytes(parserDef, file.Data, "")
  126. if err != nil {
  127. break
  128. }
  129. res.Form = formYAML
  130. break
  131. }
  132. }
  133. // if form not populated, detect common charts
  134. if res.Form == nil {
  135. // for now just case by name
  136. if res.Release.Chart.Name() == "velero" {
  137. formYAML, err := parser.FormYAMLFromBytes(parserDef, []byte(veleroForm), "")
  138. if err == nil {
  139. res.Form = formYAML
  140. }
  141. }
  142. }
  143. if err := json.NewEncoder(w).Encode(res); err != nil {
  144. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  145. return
  146. }
  147. }
  148. // HandleGetReleaseComponents retrieves kubernetes objects listed in a release identified by name and revision
  149. func (app *App) HandleGetReleaseComponents(w http.ResponseWriter, r *http.Request) {
  150. name := chi.URLParam(r, "name")
  151. revision, err := strconv.ParseUint(chi.URLParam(r, "revision"), 0, 64)
  152. form := &forms.GetReleaseForm{
  153. ReleaseForm: &forms.ReleaseForm{
  154. Form: &helm.Form{
  155. Repo: app.Repo,
  156. },
  157. },
  158. Name: name,
  159. Revision: int(revision),
  160. }
  161. agent, err := app.getAgentFromQueryParams(
  162. w,
  163. r,
  164. form.ReleaseForm,
  165. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  166. )
  167. // errors are handled in app.getAgentFromQueryParams
  168. if err != nil {
  169. return
  170. }
  171. release, err := agent.GetRelease(form.Name, form.Revision)
  172. if err != nil {
  173. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  174. Code: ErrReleaseReadData,
  175. Errors: []string{"release not found"},
  176. }, w)
  177. return
  178. }
  179. yamlArr := grapher.ImportMultiDocYAML([]byte(release.Manifest))
  180. objects := grapher.ParseObjs(yamlArr)
  181. parsed := grapher.ParsedObjs{
  182. Objects: objects,
  183. }
  184. parsed.GetControlRel()
  185. parsed.GetLabelRel()
  186. parsed.GetSpecRel()
  187. if err := json.NewEncoder(w).Encode(parsed); err != nil {
  188. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  189. return
  190. }
  191. }
  192. // HandleGetReleaseControllers retrieves controllers that belong to a release.
  193. // Used to display status of charts.
  194. func (app *App) HandleGetReleaseControllers(w http.ResponseWriter, r *http.Request) {
  195. name := chi.URLParam(r, "name")
  196. revision, err := strconv.ParseUint(chi.URLParam(r, "revision"), 0, 64)
  197. form := &forms.GetReleaseForm{
  198. ReleaseForm: &forms.ReleaseForm{
  199. Form: &helm.Form{
  200. Repo: app.Repo,
  201. },
  202. },
  203. Name: name,
  204. Revision: int(revision),
  205. }
  206. agent, err := app.getAgentFromQueryParams(
  207. w,
  208. r,
  209. form.ReleaseForm,
  210. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  211. )
  212. // errors are handled in app.getAgentFromQueryParams
  213. if err != nil {
  214. return
  215. }
  216. release, err := agent.GetRelease(form.Name, form.Revision)
  217. if err != nil {
  218. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  219. Code: ErrReleaseReadData,
  220. Errors: []string{"release not found"},
  221. }, w)
  222. return
  223. }
  224. vals, err := url.ParseQuery(r.URL.RawQuery)
  225. if err != nil {
  226. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  227. return
  228. }
  229. // get the filter options
  230. k8sForm := &forms.K8sForm{
  231. OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
  232. Repo: app.Repo,
  233. },
  234. }
  235. k8sForm.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
  236. // validate the form
  237. if err := app.validator.Struct(k8sForm); err != nil {
  238. app.handleErrorFormValidation(err, ErrK8sValidate, w)
  239. return
  240. }
  241. // create a new kubernetes agent
  242. var k8sAgent *kubernetes.Agent
  243. if app.ServerConf.IsTesting {
  244. k8sAgent = app.TestAgents.K8sAgent
  245. } else {
  246. k8sAgent, err = kubernetes.GetAgentOutOfClusterConfig(k8sForm.OutOfClusterConfig)
  247. }
  248. yamlArr := grapher.ImportMultiDocYAML([]byte(release.Manifest))
  249. controllers := grapher.ParseControllers(yamlArr)
  250. retrievedControllers := []interface{}{}
  251. // get current status of each controller
  252. // TODO: refactor with type assertion
  253. for _, c := range controllers {
  254. c.Namespace = form.ReleaseForm.Form.Namespace
  255. switch c.Kind {
  256. case "Deployment":
  257. rc, err := k8sAgent.GetDeployment(c)
  258. if err != nil {
  259. app.handleErrorDataRead(err, w)
  260. return
  261. }
  262. rc.Kind = c.Kind
  263. retrievedControllers = append(retrievedControllers, rc)
  264. case "StatefulSet":
  265. rc, err := k8sAgent.GetStatefulSet(c)
  266. if err != nil {
  267. app.handleErrorDataRead(err, w)
  268. return
  269. }
  270. rc.Kind = c.Kind
  271. retrievedControllers = append(retrievedControllers, rc)
  272. case "DaemonSet":
  273. rc, err := k8sAgent.GetDaemonSet(c)
  274. if err != nil {
  275. app.handleErrorDataRead(err, w)
  276. return
  277. }
  278. rc.Kind = c.Kind
  279. retrievedControllers = append(retrievedControllers, rc)
  280. case "ReplicaSet":
  281. rc, err := k8sAgent.GetReplicaSet(c)
  282. if err != nil {
  283. app.handleErrorDataRead(err, w)
  284. return
  285. }
  286. rc.Kind = c.Kind
  287. retrievedControllers = append(retrievedControllers, rc)
  288. }
  289. }
  290. if err := json.NewEncoder(w).Encode(retrievedControllers); err != nil {
  291. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  292. return
  293. }
  294. }
  295. // HandleListReleaseHistory retrieves a history of releases based on a release name
  296. func (app *App) HandleListReleaseHistory(w http.ResponseWriter, r *http.Request) {
  297. name := chi.URLParam(r, "name")
  298. form := &forms.ListReleaseHistoryForm{
  299. ReleaseForm: &forms.ReleaseForm{
  300. Form: &helm.Form{
  301. Repo: app.Repo,
  302. },
  303. },
  304. Name: name,
  305. }
  306. agent, err := app.getAgentFromQueryParams(
  307. w,
  308. r,
  309. form.ReleaseForm,
  310. form.ReleaseForm.PopulateHelmOptionsFromQueryParams,
  311. )
  312. // errors are handled in app.getAgentFromQueryParams
  313. if err != nil {
  314. return
  315. }
  316. release, err := agent.GetReleaseHistory(form.Name)
  317. if err != nil {
  318. app.sendExternalError(err, http.StatusNotFound, HTTPError{
  319. Code: ErrReleaseReadData,
  320. Errors: []string{"release not found"},
  321. }, w)
  322. return
  323. }
  324. if err := json.NewEncoder(w).Encode(release); err != nil {
  325. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  326. return
  327. }
  328. }
  329. // HandleGetReleaseToken retrieves the webhook token of a specific release.
  330. func (app *App) HandleGetReleaseToken(w http.ResponseWriter, r *http.Request) {
  331. name := chi.URLParam(r, "name")
  332. vals, err := url.ParseQuery(r.URL.RawQuery)
  333. namespace := vals["namespace"][0]
  334. release, err := app.Repo.Release.ReadRelease(name, namespace)
  335. if err != nil {
  336. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  337. Code: ErrReleaseReadData,
  338. Errors: []string{"release not found"},
  339. }, w)
  340. }
  341. releaseExt := release.Externalize()
  342. if err := json.NewEncoder(w).Encode(releaseExt); err != nil {
  343. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  344. return
  345. }
  346. }
  347. // HandleUpgradeRelease upgrades a release with new values.yaml
  348. func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
  349. name := chi.URLParam(r, "name")
  350. vals, err := url.ParseQuery(r.URL.RawQuery)
  351. if err != nil {
  352. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  353. return
  354. }
  355. form := &forms.UpgradeReleaseForm{
  356. ReleaseForm: &forms.ReleaseForm{
  357. Form: &helm.Form{
  358. Repo: app.Repo,
  359. },
  360. },
  361. Name: name,
  362. }
  363. form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
  364. vals,
  365. app.Repo.Cluster,
  366. )
  367. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  368. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  369. return
  370. }
  371. agent, err := app.getAgentFromReleaseForm(
  372. w,
  373. r,
  374. form.ReleaseForm,
  375. )
  376. // errors are handled in app.getAgentFromBodyParams
  377. if err != nil {
  378. return
  379. }
  380. _, err = agent.UpgradeRelease(form.Name, form.Values)
  381. if err != nil {
  382. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  383. Code: ErrReleaseDeploy,
  384. Errors: []string{"error upgrading release " + err.Error()},
  385. }, w)
  386. return
  387. }
  388. w.WriteHeader(http.StatusOK)
  389. }
  390. // HandleReleaseDeployHook upgrades a release with new image commit
  391. func (app *App) HandleReleaseDeployHook(w http.ResponseWriter, r *http.Request) {
  392. name := chi.URLParam(r, "name")
  393. vals, err := url.ParseQuery(r.URL.RawQuery)
  394. commit := vals["commit"][0]
  395. repository := vals["repository"][0]
  396. if err != nil {
  397. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  398. return
  399. }
  400. form := &forms.UpgradeReleaseForm{
  401. ReleaseForm: &forms.ReleaseForm{
  402. Form: &helm.Form{
  403. Repo: app.Repo,
  404. },
  405. },
  406. Name: name,
  407. }
  408. form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
  409. vals,
  410. app.Repo.Cluster,
  411. )
  412. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  413. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  414. return
  415. }
  416. agent, err := app.getAgentFromReleaseForm(
  417. w,
  418. r,
  419. form.ReleaseForm,
  420. )
  421. // errors are handled in app.getAgentFromBodyParams
  422. if err != nil {
  423. return
  424. }
  425. image := map[string]interface{}{}
  426. image["repository"] = repository
  427. image["tag"] = commit
  428. newval := map[string]interface{}{}
  429. newval["image"] = image
  430. _, err = agent.UpgradeReleaseByValues(form.Name, newval)
  431. if err != nil {
  432. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  433. Code: ErrReleaseDeploy,
  434. Errors: []string{"error upgrading release " + err.Error()},
  435. }, w)
  436. return
  437. }
  438. w.WriteHeader(http.StatusOK)
  439. }
  440. // HandleReleaseDeployWebhook upgrades a release when a chart specific webhook is called.
  441. func (app *App) HandleReleaseDeployWebhook(w http.ResponseWriter, r *http.Request) {
  442. token := chi.URLParam(r, "token")
  443. // retrieve release by token
  444. release, err := app.Repo.Release.ReadReleaseByWebhookToken(token)
  445. if err != nil {
  446. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  447. Code: ErrReleaseReadData,
  448. Errors: []string{"release not found with given webhook"},
  449. }, w)
  450. return
  451. }
  452. params := map[string][]string{}
  453. params["cluster_id"] = []string{fmt.Sprint(release.ClusterID)}
  454. params["storage"] = []string{"secret"}
  455. params["namespace"] = []string{release.Namespace}
  456. vals, err := url.ParseQuery(r.URL.RawQuery)
  457. commit := vals["commit"][0]
  458. repository := vals["repository"][0]
  459. if err != nil {
  460. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  461. return
  462. }
  463. form := &forms.UpgradeReleaseForm{
  464. ReleaseForm: &forms.ReleaseForm{
  465. Form: &helm.Form{
  466. Repo: app.Repo,
  467. },
  468. },
  469. Name: release.Name,
  470. }
  471. form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
  472. params,
  473. app.Repo.Cluster,
  474. )
  475. agent, err := app.getAgentFromReleaseForm(
  476. w,
  477. r,
  478. form.ReleaseForm,
  479. )
  480. // errors are handled in app.getAgentFromBodyParams
  481. if err != nil {
  482. return
  483. }
  484. image := map[string]interface{}{}
  485. image["repository"] = repository
  486. image["tag"] = commit
  487. newval := map[string]interface{}{}
  488. newval["image"] = image
  489. _, err = agent.UpgradeReleaseByValues(form.Name, newval)
  490. if err != nil {
  491. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  492. Code: ErrReleaseDeploy,
  493. Errors: []string{"error upgrading release " + err.Error()},
  494. }, w)
  495. return
  496. }
  497. w.WriteHeader(http.StatusOK)
  498. }
  499. // HandleRollbackRelease rolls a release back to a specified revision
  500. func (app *App) HandleRollbackRelease(w http.ResponseWriter, r *http.Request) {
  501. name := chi.URLParam(r, "name")
  502. vals, err := url.ParseQuery(r.URL.RawQuery)
  503. if err != nil {
  504. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  505. return
  506. }
  507. form := &forms.RollbackReleaseForm{
  508. ReleaseForm: &forms.ReleaseForm{
  509. Form: &helm.Form{
  510. Repo: app.Repo,
  511. },
  512. },
  513. Name: name,
  514. }
  515. form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
  516. vals,
  517. app.Repo.Cluster,
  518. )
  519. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  520. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  521. return
  522. }
  523. agent, err := app.getAgentFromReleaseForm(
  524. w,
  525. r,
  526. form.ReleaseForm,
  527. )
  528. // errors are handled in app.getAgentFromBodyParams
  529. if err != nil {
  530. return
  531. }
  532. err = agent.RollbackRelease(form.Name, form.Revision)
  533. if err != nil {
  534. app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
  535. Code: ErrReleaseDeploy,
  536. Errors: []string{"error rolling back release " + err.Error()},
  537. }, w)
  538. return
  539. }
  540. w.WriteHeader(http.StatusOK)
  541. }
  542. // ------------------------ Release handler helper functions ------------------------ //
  543. // getAgentFromQueryParams uses the query params to populate a form, and then
  544. // passes that form to the underlying app.getAgentFromReleaseForm to create a new
  545. // Helm agent.
  546. func (app *App) getAgentFromQueryParams(
  547. w http.ResponseWriter,
  548. r *http.Request,
  549. form *forms.ReleaseForm,
  550. // populate uses the query params to populate a form
  551. populate ...func(vals url.Values, repo repository.ClusterRepository) error,
  552. ) (*helm.Agent, error) {
  553. vals, err := url.ParseQuery(r.URL.RawQuery)
  554. if err != nil {
  555. app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
  556. return nil, err
  557. }
  558. for _, f := range populate {
  559. err := f(vals, app.Repo.Cluster)
  560. if err != nil {
  561. return nil, err
  562. }
  563. }
  564. return app.getAgentFromReleaseForm(w, r, form)
  565. }
  566. // getAgentFromReleaseForm uses a non-validated form to construct a new Helm agent based on
  567. // the userID found in the session and the options required by the Helm agent.
  568. func (app *App) getAgentFromReleaseForm(
  569. w http.ResponseWriter,
  570. r *http.Request,
  571. form *forms.ReleaseForm,
  572. ) (*helm.Agent, error) {
  573. var err error
  574. // validate the form
  575. if err := app.validator.Struct(form); err != nil {
  576. app.handleErrorFormValidation(err, ErrReleaseValidateFields, w)
  577. return nil, err
  578. }
  579. // create a new agent
  580. var agent *helm.Agent
  581. if app.ServerConf.IsTesting {
  582. agent = app.TestAgents.HelmAgent
  583. } else {
  584. agent, err = helm.GetAgentOutOfClusterConfig(form.Form, app.Logger)
  585. }
  586. return agent, err
  587. }
  588. const veleroForm string = `tags:
  589. - hello
  590. tabs:
  591. - name: main
  592. context:
  593. type: cluster
  594. config:
  595. group: velero.io
  596. version: v1
  597. resource: backups
  598. label: Backups
  599. sections:
  600. - name: section_one
  601. contents:
  602. - type: heading
  603. label: 💾 Velero Backups
  604. - type: resource-list
  605. value: |
  606. .items[] | {
  607. name: .metadata.name,
  608. label: .metadata.namespace,
  609. status: .status.phase,
  610. timestamp: .status.completionTimestamp,
  611. message: [
  612. (if .status.volumeSnapshotsAttempted then "\(.status.volumeSnapshotsAttempted) volume snapshots attempted, \(.status.volumeSnapshotsCompleted) completed." else null end),
  613. "Finished \(.status.completionTimestamp).",
  614. "Backup expires on \(.status.expiration)."
  615. ]|join(" "),
  616. data: {
  617. "Included Namespaces": (if .spec.includedNamespaces then .spec.includedNamespaces|join(",") else "* (all)" end),
  618. "Included Resources": (if .spec.includedResources then .spec.includedResources|join(",") else "* (all)" end),
  619. "Storage Location": .spec.storageLocation
  620. }
  621. }`