yaml_from_revision.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. package porter_app
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "net/http"
  6. "strings"
  7. "github.com/porter-dev/porter/internal/kubernetes"
  8. "github.com/google/uuid"
  9. "github.com/porter-dev/api-contracts/generated/go/helpers"
  10. "github.com/porter-dev/api-contracts/generated/go/porter/v1/porterv1connect"
  11. "github.com/porter-dev/porter/internal/deployment_target"
  12. "github.com/porter-dev/porter/internal/repository"
  13. "github.com/porter-dev/porter/internal/porter_app"
  14. "connectrpc.com/connect"
  15. porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
  16. "gopkg.in/yaml.v2"
  17. v2 "github.com/porter-dev/porter/internal/porter_app/v2"
  18. "github.com/porter-dev/porter/internal/telemetry"
  19. "github.com/porter-dev/porter/api/server/authz"
  20. "github.com/porter-dev/porter/api/server/handlers"
  21. "github.com/porter-dev/porter/api/server/shared"
  22. "github.com/porter-dev/porter/api/server/shared/apierrors"
  23. "github.com/porter-dev/porter/api/server/shared/config"
  24. "github.com/porter-dev/porter/api/server/shared/requestutils"
  25. "github.com/porter-dev/porter/api/types"
  26. "github.com/porter-dev/porter/internal/models"
  27. )
  28. // PorterYAMLFromRevisionHandler is the handler for the /apps/{porter_app_name}/revisions/{app_revision_id}/yaml endpoint
  29. type PorterYAMLFromRevisionHandler struct {
  30. handlers.PorterHandlerReadWriter
  31. authz.KubernetesAgentGetter
  32. }
  33. // NewPorterYAMLFromRevisionHandler returns a new PorterYAMLFromRevisionHandler
  34. func NewPorterYAMLFromRevisionHandler(
  35. config *config.Config,
  36. decoderValidator shared.RequestDecoderValidator,
  37. writer shared.ResultWriter,
  38. ) *PorterYAMLFromRevisionHandler {
  39. return &PorterYAMLFromRevisionHandler{
  40. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  41. KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
  42. }
  43. }
  44. // PorterYAMLFromRevisionRequest is the request object for the /apps/{porter_app_name}/revisions/{app_revision_id}/yaml endpoint
  45. type PorterYAMLFromRevisionRequest struct {
  46. ShouldFormatForExport bool `schema:"should_format_for_export"`
  47. }
  48. // PorterYAMLFromRevisionResponse is the response object for the /apps/{porter_app_name}/revisions/{app_revision_id}/yaml endpoint
  49. type PorterYAMLFromRevisionResponse struct {
  50. B64PorterYAML string `json:"b64_porter_yaml"`
  51. }
  52. // ServeHTTP takes a porter app revision and returns the porter yaml for it
  53. func (c *PorterYAMLFromRevisionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  54. ctx, span := telemetry.NewSpan(r.Context(), "serve-porter-yaml-from-revision")
  55. defer span.End()
  56. r = r.Clone(ctx)
  57. project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
  58. cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
  59. appRevisionID, reqErr := requestutils.GetURLParamString(r, types.URLParamAppRevisionID)
  60. if reqErr != nil {
  61. err := telemetry.Error(ctx, span, nil, "error parsing app revision id")
  62. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  63. return
  64. }
  65. request := &PorterYAMLFromRevisionRequest{}
  66. if ok := c.DecodeAndValidate(w, r, request); !ok {
  67. err := telemetry.Error(ctx, span, nil, "error decoding request")
  68. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  69. return
  70. }
  71. getRevisionReq := connect.NewRequest(&porterv1.GetAppRevisionRequest{
  72. ProjectId: int64(project.ID),
  73. AppRevisionId: appRevisionID,
  74. })
  75. ccpResp, err := c.Config().ClusterControlPlaneClient.GetAppRevision(ctx, getRevisionReq)
  76. if err != nil {
  77. err = telemetry.Error(ctx, span, err, "error getting app revision")
  78. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  79. return
  80. }
  81. if ccpResp == nil || ccpResp.Msg == nil {
  82. err = telemetry.Error(ctx, span, nil, "get app revision response is nil")
  83. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  84. return
  85. }
  86. if ccpResp.Msg.AppRevision == nil {
  87. err = telemetry.Error(ctx, span, nil, "app revision is nil")
  88. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  89. return
  90. }
  91. appProto := ccpResp.Msg.AppRevision.App
  92. if appProto == nil {
  93. err = telemetry.Error(ctx, span, nil, "app proto is nil")
  94. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  95. return
  96. }
  97. agent, err := c.GetAgent(r, cluster, "")
  98. if err != nil {
  99. err = telemetry.Error(ctx, span, err, "error getting agent for cluster")
  100. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  101. return
  102. }
  103. appRevisionUUID, err := uuid.Parse(appRevisionID)
  104. if err != nil {
  105. err = telemetry.Error(ctx, span, err, "error parsing app revision id")
  106. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  107. return
  108. }
  109. env, defaultEnvGroupName, err := defaultEnvGroup(ctx, formatDefaultEnvGroupInput{
  110. ProjectID: project.ID,
  111. Cluster: cluster,
  112. AppRevisionID: appRevisionUUID,
  113. appYAML: v2.PorterApp{},
  114. K8sAgent: agent,
  115. ClusterControlPlaneClient: c.Config().ClusterControlPlaneClient,
  116. PorterAppRepository: c.Repo().PorterApp(),
  117. })
  118. if err != nil {
  119. err = telemetry.Error(ctx, span, err, "error formatting default env group")
  120. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  121. return
  122. }
  123. app, err := v2.AppFromProto(appProto)
  124. if err != nil {
  125. err = telemetry.Error(ctx, span, err, "error converting app proto to porter yaml")
  126. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  127. return
  128. }
  129. var envGroups []string
  130. for _, envGroup := range app.EnvGroups {
  131. if !strings.Contains(envGroup, defaultEnvGroupName) {
  132. envGroups = append(envGroups, envGroup)
  133. }
  134. }
  135. app.Env = env
  136. app.EnvGroups = envGroups
  137. app = zeroOutValues(app)
  138. if request.ShouldFormatForExport {
  139. app = formatForExport(app, c.Config().ServerConf.AppRootDomain)
  140. }
  141. porterYAMLString, err := yaml.Marshal(app)
  142. if err != nil {
  143. err = telemetry.Error(ctx, span, err, "error marshaling porter yaml")
  144. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  145. return
  146. }
  147. b64String := base64.StdEncoding.EncodeToString(porterYAMLString)
  148. response := &PorterYAMLFromRevisionResponse{
  149. B64PorterYAML: b64String,
  150. }
  151. c.WriteResult(w, r, response)
  152. }
  153. type formatDefaultEnvGroupInput struct {
  154. ProjectID uint
  155. Cluster *models.Cluster
  156. AppRevisionID uuid.UUID
  157. appYAML v2.PorterApp
  158. K8sAgent *kubernetes.Agent
  159. ClusterControlPlaneClient porterv1connect.ClusterControlPlaneServiceClient
  160. PorterAppRepository repository.PorterAppRepository
  161. }
  162. func defaultEnvGroup(ctx context.Context, input formatDefaultEnvGroupInput) (map[string]string, string, error) {
  163. ctx, span := telemetry.NewSpan(ctx, "format-default-env-group")
  164. defer span.End()
  165. env := map[string]string{}
  166. revision, err := porter_app.GetAppRevision(ctx, porter_app.GetAppRevisionInput{
  167. AppRevisionID: input.AppRevisionID,
  168. ProjectID: input.ProjectID,
  169. CCPClient: input.ClusterControlPlaneClient,
  170. })
  171. if err != nil {
  172. return env, "", telemetry.Error(ctx, span, err, "error getting app revision")
  173. }
  174. decoded, err := base64.StdEncoding.DecodeString(revision.B64AppProto)
  175. if err != nil {
  176. return env, "", telemetry.Error(ctx, span, err, "error decoding base proto")
  177. }
  178. appProto := &porterv1.PorterApp{}
  179. err = helpers.UnmarshalContractObject(decoded, appProto)
  180. if err != nil {
  181. return env, "", telemetry.Error(ctx, span, err, "error unmarshalling app proto")
  182. }
  183. deploymentTarget, err := deployment_target.DeploymentTargetDetails(ctx, deployment_target.DeploymentTargetDetailsInput{
  184. ProjectID: int64(input.ProjectID),
  185. ClusterID: int64(input.Cluster.ID),
  186. DeploymentTargetID: revision.DeploymentTarget.ID,
  187. CCPClient: input.ClusterControlPlaneClient,
  188. })
  189. if err != nil {
  190. return env, "", telemetry.Error(ctx, span, err, "error getting deployment target details")
  191. }
  192. revisionWithEnv, err := porter_app.AttachEnvToRevision(ctx, porter_app.AttachEnvToRevisionInput{
  193. ProjectID: input.ProjectID,
  194. ClusterID: int(input.Cluster.ID),
  195. Revision: revision,
  196. DeploymentTarget: deploymentTarget,
  197. K8SAgent: input.K8sAgent,
  198. PorterAppRepository: input.PorterAppRepository,
  199. })
  200. if err != nil {
  201. return env, "", telemetry.Error(ctx, span, err, "error attaching env to revision")
  202. }
  203. for key, val := range revisionWithEnv.Env.Variables {
  204. env[key] = val
  205. }
  206. for key, val := range revisionWithEnv.Env.SecretVariables {
  207. env[key] = val
  208. }
  209. return env, revisionWithEnv.Env.Name, nil
  210. }
  211. func formatForExport(app v2.PorterApp, appRootDomain string) v2.PorterApp {
  212. for i := range app.Services {
  213. app.Services[i] = filterNewServiceValues(app.Services[i])
  214. if app.Services[i].Type == v2.ServiceType_Web {
  215. // remove porter domains
  216. var filteredDomains []v2.Domains
  217. for _, domain := range app.Services[i].Domains {
  218. if !strings.HasSuffix(domain.Name, appRootDomain) {
  219. filteredDomains = append(filteredDomains, domain)
  220. }
  221. }
  222. app.Services[i].Domains = filteredDomains
  223. }
  224. }
  225. if app.Predeploy != nil {
  226. predeploy := filterNewServiceValues(*app.Predeploy)
  227. app.Predeploy = &predeploy
  228. }
  229. // don't show image or commit sha if build is present
  230. if app.Build != nil {
  231. app.Image = nil
  232. app.Build.CommitSHA = ""
  233. }
  234. // remove env secrets from env
  235. for key, val := range app.Env {
  236. if val == "********" {
  237. delete(app.Env, key)
  238. }
  239. }
  240. // don't show env group versions
  241. for i := range app.EnvGroups {
  242. app.EnvGroups[i] = strings.Split(app.EnvGroups[i], ":")[0]
  243. }
  244. return app
  245. }
  246. // this "no-op" ensures that new fields are always zero-ed out in the exported yaml, until we specifically add it here
  247. func filterNewServiceValues(service v2.Service) v2.Service {
  248. return v2.Service{
  249. Name: service.Name,
  250. Run: service.Run,
  251. Type: service.Type,
  252. Instances: service.Instances,
  253. CpuCores: service.CpuCores,
  254. RamMegabytes: service.RamMegabytes,
  255. GpuCoresNvidia: service.GpuCoresNvidia,
  256. GPU: service.GPU,
  257. SmartOptimization: service.SmartOptimization,
  258. TerminationGracePeriodSeconds: service.TerminationGracePeriodSeconds,
  259. Port: service.Port,
  260. Autoscaling: service.Autoscaling,
  261. Domains: service.Domains,
  262. HealthCheck: service.HealthCheck,
  263. AllowConcurrent: service.AllowConcurrent,
  264. Cron: service.Cron,
  265. SuspendCron: service.SuspendCron,
  266. TimeoutSeconds: service.TimeoutSeconds,
  267. Private: service.Private,
  268. IngressAnnotations: service.IngressAnnotations,
  269. DisableTLS: service.DisableTLS,
  270. }
  271. }
  272. func zeroOutValues(app v2.PorterApp) v2.PorterApp {
  273. for i := range app.Services {
  274. // remove smart optimization
  275. app.Services[i].SmartOptimization = nil
  276. if app.Services[i].GPU != nil && !app.Services[i].GPU.Enabled {
  277. app.Services[i].GPU = nil
  278. }
  279. // remove launcher
  280. if app.Services[i].Run != nil {
  281. launcherLess := strings.TrimPrefix(*app.Services[i].Run, "launcher ")
  282. launcherLess = strings.TrimPrefix(launcherLess, "/cnb/lifecycle/launcher ")
  283. app.Services[i].Run = &launcherLess
  284. }
  285. switch app.Services[i].Type {
  286. case v2.ServiceType_Web:
  287. // remove autoscaling if not enabled
  288. if app.Services[i].Autoscaling != nil && !app.Services[i].Autoscaling.Enabled {
  289. app.Services[i].Autoscaling = nil
  290. }
  291. // remove health if not enabled
  292. if app.Services[i].HealthCheck != nil && !app.Services[i].HealthCheck.Enabled {
  293. app.Services[i].HealthCheck = nil
  294. }
  295. // don't show disableTLS if not enabled
  296. if app.Services[i].DisableTLS != nil && !*app.Services[i].DisableTLS {
  297. app.Services[i].DisableTLS = nil
  298. }
  299. // remove private if not enabled
  300. if app.Services[i].Private != nil && !*app.Services[i].Private {
  301. app.Services[i].Private = nil
  302. }
  303. case v2.ServiceType_Worker:
  304. // remove autoscaling if not enabled
  305. if app.Services[i].Autoscaling != nil && !app.Services[i].Autoscaling.Enabled {
  306. app.Services[i].Autoscaling = nil
  307. }
  308. // remove port
  309. app.Services[i].Port = 0
  310. case v2.ServiceType_Job:
  311. // remove port
  312. app.Services[i].Port = 0
  313. // remove instances
  314. app.Services[i].Instances = nil
  315. // remove suspendCron if not enabled
  316. if app.Services[i].SuspendCron != nil && !*app.Services[i].SuspendCron {
  317. app.Services[i].SuspendCron = nil
  318. }
  319. // remove allowConcurrency if not enabled
  320. if app.Services[i].AllowConcurrent != nil && !*app.Services[i].AllowConcurrent {
  321. app.Services[i].AllowConcurrent = nil
  322. }
  323. }
  324. }
  325. if app.Predeploy != nil {
  326. // remove name
  327. app.Predeploy.Name = ""
  328. // remove type
  329. app.Predeploy.Type = ""
  330. // remove smart optimization
  331. app.Predeploy.SmartOptimization = nil
  332. // remove launcher
  333. if app.Predeploy.Run != nil {
  334. launcherLess := strings.TrimPrefix(*app.Predeploy.Run, "launcher ")
  335. launcherLess = strings.TrimPrefix(launcherLess, "/cnb/lifecycle/launcher ")
  336. app.Predeploy.Run = &launcherLess
  337. }
  338. // remove port
  339. app.Predeploy.Port = 0
  340. // remove instances
  341. app.Predeploy.Instances = nil
  342. // remove suspendCron
  343. app.Predeploy.SuspendCron = nil
  344. // remove allowConcurrency
  345. app.Predeploy.AllowConcurrent = nil
  346. // remove timeout
  347. app.Predeploy.TimeoutSeconds = 0
  348. }
  349. return app
  350. }