yaml_from_revision.go 17 KB

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