get.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. package release
  2. import (
  3. "net/http"
  4. semver "github.com/Masterminds/semver/v3"
  5. "github.com/porter-dev/porter/api/server/authz"
  6. "github.com/porter-dev/porter/api/server/handlers"
  7. "github.com/porter-dev/porter/api/server/shared"
  8. "github.com/porter-dev/porter/api/server/shared/apierrors"
  9. "github.com/porter-dev/porter/api/server/shared/config"
  10. "github.com/porter-dev/porter/api/types"
  11. "github.com/porter-dev/porter/internal/helm/loader"
  12. "github.com/porter-dev/porter/internal/models"
  13. "github.com/porter-dev/porter/internal/telemetry"
  14. "github.com/porter-dev/porter/internal/templater/parser"
  15. "github.com/stefanmcshane/helm/pkg/release"
  16. "gorm.io/gorm"
  17. )
  18. type ReleaseGetHandler struct {
  19. handlers.PorterHandlerWriter
  20. authz.KubernetesAgentGetter
  21. }
  22. func NewReleaseGetHandler(
  23. config *config.Config,
  24. writer shared.ResultWriter,
  25. ) *ReleaseGetHandler {
  26. return &ReleaseGetHandler{
  27. PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
  28. KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
  29. }
  30. }
  31. func (c *ReleaseGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  32. ctx, span := telemetry.NewSpan(r.Context(), "serve-get-release")
  33. defer span.End()
  34. helmRelease, _ := ctx.Value(types.ReleaseScope).(*release.Release)
  35. cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
  36. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "release-name", Value: helmRelease.Name})
  37. res := &types.Release{
  38. Release: helmRelease,
  39. }
  40. // look up the release in the database; if not found, do not populate Porter fields
  41. release, err := c.Repo().Release().ReadRelease(cluster.ID, helmRelease.Name, helmRelease.Namespace)
  42. if err == nil {
  43. res.PorterRelease = release.ToReleaseType()
  44. res.ID = release.ID
  45. res.WebhookToken = release.WebhookToken
  46. if release.GitActionConfig != nil {
  47. res.GitActionConfig = release.GitActionConfig.ToGitActionConfigType()
  48. }
  49. if release.BuildConfig != 0 {
  50. bc, err := c.Repo().BuildConfig().GetBuildConfig(release.BuildConfig)
  51. if err != nil {
  52. err = telemetry.Error(ctx, span, err, "unable to get build config")
  53. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  54. return
  55. }
  56. res.BuildConfig = bc.ToBuildConfigType()
  57. }
  58. if release.StackResourceID != 0 {
  59. stackResource, err := c.Repo().Stack().ReadStackResource(release.StackResourceID)
  60. if err != nil {
  61. err = telemetry.Error(ctx, span, err, "unable to get stack resource")
  62. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  63. return
  64. }
  65. stackRevision, err := c.Repo().Stack().ReadStackRevision(stackResource.StackRevisionID)
  66. if err != nil {
  67. err = telemetry.Error(ctx, span, err, "unable to get stack revision")
  68. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  69. return
  70. }
  71. stack, err := c.Repo().Stack().ReadStackByID(cluster.ProjectID, stackRevision.StackID)
  72. if err != nil {
  73. err = telemetry.Error(ctx, span, err, "unable to get stack")
  74. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  75. return
  76. }
  77. res.StackID = stack.UID
  78. }
  79. } else if err != gorm.ErrRecordNotFound {
  80. err = telemetry.Error(ctx, span, err, "unable to get release")
  81. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  82. return
  83. } else {
  84. res.PorterRelease = &types.PorterRelease{}
  85. }
  86. // detect if Porter application chart and attempt to get the latest version
  87. // from chart repo
  88. cache := c.Config().URLCache
  89. chartRepoURL, foundFirst := cache.GetURL(helmRelease.Chart.Metadata.Name)
  90. if !foundFirst {
  91. cache.Update()
  92. chartRepoURL, _ = cache.GetURL(helmRelease.Chart.Metadata.Name)
  93. }
  94. if chartRepoURL != "" {
  95. repoIndex, err := loader.LoadRepoIndexPublic(chartRepoURL)
  96. if err == nil {
  97. porterChart := loader.FindPorterChartInIndexList(repoIndex, res.Chart.Metadata.Name)
  98. res.LatestVersion = res.Chart.Metadata.Version
  99. // set latest version to the greater of porterChart.Versions and res.Chart.Metadata.Version
  100. porterChartVersion, porterChartErr := semver.NewVersion(porterChart.Versions[0])
  101. currChartVersion, currChartErr := semver.NewVersion(res.Chart.Metadata.Version)
  102. if currChartErr == nil && porterChartErr == nil && porterChartVersion.GreaterThan(currChartVersion) {
  103. res.LatestVersion = porterChart.Versions[0]
  104. }
  105. }
  106. }
  107. // look for the form using the dynamic client
  108. dynClient, err := c.GetDynamicClient(r, cluster)
  109. if err != nil {
  110. err = telemetry.Error(ctx, span, err, "unable to get dynamic client")
  111. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  112. return
  113. }
  114. parserDef := &parser.ClientConfigDefault{
  115. DynamicClient: dynClient,
  116. HelmChart: helmRelease.Chart,
  117. HelmRelease: helmRelease,
  118. }
  119. form, err := parser.GetFormFromRelease(parserDef, helmRelease)
  120. if err != nil {
  121. c.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(err))
  122. } else {
  123. res.Form = form
  124. }
  125. // if form not populated, detect common charts
  126. if res.Form == nil {
  127. // for now just case by name
  128. if res.Release.Chart.Name() == "cert-manager" {
  129. formYAML, err := parser.FormYAMLFromBytes(parserDef, []byte(certManagerForm), "", "")
  130. if err == nil {
  131. res.Form = formYAML
  132. }
  133. } else if res.Release.Chart.Name() == "velero" {
  134. formYAML, err := parser.FormYAMLFromBytes(parserDef, []byte(veleroForm), "", "")
  135. if err == nil {
  136. res.Form = formYAML
  137. }
  138. }
  139. }
  140. c.WriteResult(w, r, res)
  141. }
  142. const certManagerForm string = `tags:
  143. - hello
  144. tabs:
  145. - name: main
  146. context:
  147. type: cluster
  148. config:
  149. group: cert-manager.io
  150. version: v1
  151. resource: certificates
  152. label: Certificates
  153. sections:
  154. - name: section_one
  155. contents:
  156. - type: heading
  157. label: Certificates
  158. - type: resource-list
  159. settings:
  160. options:
  161. resource-button:
  162. name: "Renew Certificate"
  163. description: "This will delete the existing certificate resource, triggering a new certificate request."
  164. actions:
  165. - delete:
  166. scope: namespace
  167. relative_uri: /crd
  168. context:
  169. type: cluster
  170. config:
  171. group: cert-manager.io
  172. version: v1
  173. resource: certificates
  174. value: |
  175. .items[] | {
  176. metadata: .metadata,
  177. name: "\(.spec.dnsNames | join(","))",
  178. label: "\(.metadata.namespace)/\(.metadata.name)",
  179. status: (
  180. ([.status.conditions[].type] | index("Ready")) as $index | (
  181. if $index then (
  182. if .status.conditions[$index].status == "True" then "Ready" else "Not Ready" end
  183. ) else (
  184. "Not Ready"
  185. ) end
  186. )
  187. ),
  188. timestamp: .status.conditions[0].lastTransitionTime,
  189. message: [.status.conditions[].message] | unique | join(","),
  190. data: {}
  191. }`
  192. const veleroForm string = `tags:
  193. - hello
  194. tabs:
  195. - name: main
  196. context:
  197. type: cluster
  198. config:
  199. group: velero.io
  200. version: v1
  201. resource: backups
  202. label: Backups
  203. sections:
  204. - name: section_one
  205. contents:
  206. - type: heading
  207. label: 💾 Velero Backups
  208. - type: resource-list
  209. value: |
  210. .items[] | {
  211. name: .metadata.name,
  212. label: .metadata.namespace,
  213. status: .status.phase,
  214. timestamp: .status.completionTimestamp,
  215. message: [
  216. (if .status.volumeSnapshotsAttempted then "\(.status.volumeSnapshotsAttempted) volume snapshots attempted, \(.status.volumeSnapshotsCompleted) completed." else null end),
  217. "Finished \(.status.completionTimestamp).",
  218. "Backup expires on \(.status.expiration)."
  219. ]|join(" "),
  220. data: {
  221. "Included Namespaces": (if .spec.includedNamespaces then .spec.includedNamespaces|join(",") else "* (all)" end),
  222. "Included Resources": (if .spec.includedResources then .spec.includedResources|join(",") else "* (all)" end),
  223. "Storage Location": .spec.storageLocation
  224. }
  225. }`