create.go 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736
  1. package porter_app
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "net/http"
  9. "strings"
  10. "github.com/google/uuid"
  11. "github.com/porter-dev/porter/internal/kubernetes"
  12. "github.com/porter-dev/porter/internal/kubernetes/envgroup"
  13. "github.com/porter-dev/porter/internal/telemetry"
  14. "github.com/porter-dev/porter/api/server/authz"
  15. "github.com/porter-dev/porter/api/server/handlers"
  16. "github.com/porter-dev/porter/api/server/shared"
  17. "github.com/porter-dev/porter/api/server/shared/apierrors"
  18. "github.com/porter-dev/porter/api/server/shared/config"
  19. "github.com/porter-dev/porter/api/server/shared/features"
  20. "github.com/porter-dev/porter/api/server/shared/requestutils"
  21. "github.com/porter-dev/porter/api/types"
  22. utils "github.com/porter-dev/porter/api/utils/porter_app"
  23. "github.com/porter-dev/porter/internal/helm"
  24. "github.com/porter-dev/porter/internal/helm/loader"
  25. "github.com/porter-dev/porter/internal/models"
  26. "github.com/porter-dev/porter/internal/repository"
  27. "github.com/stefanmcshane/helm/pkg/chart"
  28. )
  29. type CreatePorterAppHandler struct {
  30. handlers.PorterHandlerReadWriter
  31. authz.KubernetesAgentGetter
  32. }
  33. func NewCreatePorterAppHandler(
  34. config *config.Config,
  35. decoderValidator shared.RequestDecoderValidator,
  36. writer shared.ResultWriter,
  37. ) *CreatePorterAppHandler {
  38. return &CreatePorterAppHandler{
  39. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  40. KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
  41. }
  42. }
  43. func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  44. ctx := r.Context()
  45. project, _ := ctx.Value(types.ProjectScope).(*models.Project)
  46. cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
  47. ctx, span := telemetry.NewSpan(r.Context(), "serve-create-porter-app")
  48. defer span.End()
  49. request := &types.CreatePorterAppRequest{}
  50. if ok := c.DecodeAndValidate(w, r, request); !ok {
  51. err := telemetry.Error(ctx, span, nil, "error decoding request")
  52. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  53. return
  54. }
  55. appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
  56. if reqErr != nil {
  57. err := telemetry.Error(ctx, span, reqErr, "error getting stack name from url")
  58. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  59. return
  60. }
  61. namespace := utils.NamespaceFromPorterAppName(appName)
  62. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "application-name", Value: appName})
  63. helmAgent, err := c.GetHelmAgent(ctx, r, cluster, namespace)
  64. if err != nil {
  65. err = telemetry.Error(ctx, span, err, "error getting helm agent")
  66. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  67. return
  68. }
  69. k8sAgent, err := c.GetAgent(r, cluster, namespace)
  70. if err != nil {
  71. err = telemetry.Error(ctx, span, err, "error getting k8s agent")
  72. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  73. return
  74. }
  75. helmRelease, err := helmAgent.GetRelease(ctx, appName, 0, false)
  76. shouldCreate := err != nil
  77. porterYamlBase64 := request.PorterYAMLBase64
  78. porterYaml, err := base64.StdEncoding.DecodeString(porterYamlBase64)
  79. if err != nil {
  80. err = telemetry.Error(ctx, span, err, "error decoding porter yaml")
  81. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  82. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  83. return
  84. }
  85. imageInfo := request.ImageInfo
  86. registries, err := c.Repo().Registry().ListRegistriesByProjectID(cluster.ProjectID)
  87. if err != nil {
  88. err = telemetry.Error(ctx, span, err, "error listing registries")
  89. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  90. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  91. return
  92. }
  93. var releaseValues map[string]interface{}
  94. var releaseDependencies []*chart.Dependency
  95. if shouldCreate || request.OverrideRelease {
  96. releaseValues = nil
  97. releaseDependencies = nil
  98. // this is required because when the front-end sends an update request with overrideRelease=true, it is unable to
  99. // get the image info from the release. unless it is explicitly provided in the request, we avoid overwriting it
  100. // by attempting to get the image info from the release or the provided helm values
  101. if helmRelease != nil && (imageInfo.Repository == "" || imageInfo.Tag == "") {
  102. if request.FullHelmValues != "" {
  103. imageInfo, err = attemptToGetImageInfoFromFullHelmValues(request.FullHelmValues)
  104. if err != nil {
  105. err = telemetry.Error(ctx, span, err, "error getting image info from full helm values")
  106. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  107. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  108. return
  109. }
  110. } else {
  111. imageInfo = attemptToGetImageInfoFromRelease(helmRelease.Config)
  112. }
  113. }
  114. } else {
  115. releaseValues = helmRelease.Config
  116. releaseDependencies = helmRelease.Chart.Metadata.Dependencies
  117. }
  118. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "image-repo", Value: imageInfo.Repository}, telemetry.AttributeKV{Key: "image-tag", Value: imageInfo.Tag})
  119. if request.Builder == "" {
  120. // attempt to get builder from db
  121. app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
  122. if err == nil {
  123. request.Builder = app.Builder
  124. }
  125. }
  126. injectLauncher := strings.Contains(request.Builder, "heroku") ||
  127. strings.Contains(request.Builder, "paketo")
  128. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "builder", Value: request.Builder})
  129. if shouldCreate {
  130. // create the namespace if it does not exist already
  131. _, err = k8sAgent.CreateNamespace(namespace, nil)
  132. if err != nil {
  133. err = telemetry.Error(ctx, span, err, "error creating namespace")
  134. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  135. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  136. return
  137. }
  138. cloneEnvGroup(c, w, r, k8sAgent, request.EnvGroups, namespace)
  139. }
  140. if imageInfo.Repository == "" || imageInfo.Tag == "" {
  141. err = telemetry.Error(ctx, span, nil, "incomplete image info provided: must provide both repository and tag")
  142. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  143. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  144. return
  145. }
  146. var addCustomNodeSelector bool
  147. if (cluster.ProvisionedBy == "CAPI" && cluster.CloudProvider == "GCP") || cluster.GCPIntegrationID != 0 {
  148. addCustomNodeSelector = true
  149. }
  150. chart, values, preDeployJobValues, err := parse(
  151. ctx,
  152. ParseConf{
  153. PorterAppName: appName,
  154. PorterYaml: porterYaml,
  155. ImageInfo: imageInfo,
  156. ServerConfig: c.Config(),
  157. ProjectID: cluster.ProjectID,
  158. UserUpdate: request.UserUpdate,
  159. EnvGroups: request.EnvGroups,
  160. EnvironmentGroups: request.EnvironmentGroups,
  161. Namespace: namespace,
  162. ExistingHelmValues: releaseValues,
  163. ExistingChartDependencies: releaseDependencies,
  164. SubdomainCreateOpts: SubdomainCreateOpts{
  165. k8sAgent: k8sAgent,
  166. dnsRepo: c.Repo().DNSRecord(),
  167. dnsClient: c.Config().DNSClient,
  168. appRootDomain: c.Config().ServerConf.AppRootDomain,
  169. stackName: appName,
  170. },
  171. InjectLauncherToStartCommand: injectLauncher,
  172. ShouldValidateHelmValues: shouldCreate,
  173. FullHelmValues: request.FullHelmValues,
  174. AddCustomNodeSelector: addCustomNodeSelector,
  175. },
  176. )
  177. if err != nil {
  178. err = telemetry.Error(ctx, span, err, "parse error")
  179. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  180. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  181. return
  182. }
  183. if shouldCreate {
  184. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "installing-application", Value: true})
  185. // create the release job chart if it does not exist (only done by front-end currently, where we set overrideRelease=true)
  186. if request.OverrideRelease && preDeployJobValues != nil {
  187. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "installing-pre-deploy-job", Value: true})
  188. conf, err := createPreDeployJobChart(
  189. ctx,
  190. appName,
  191. preDeployJobValues,
  192. c.Config().ServerConf.DefaultApplicationHelmRepoURL,
  193. registries,
  194. cluster,
  195. c.Repo(),
  196. )
  197. if err != nil {
  198. err = telemetry.Error(ctx, span, err, "error making config for pre-deploy job chart")
  199. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  200. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  201. return
  202. }
  203. _, err = helmAgent.InstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  204. if err != nil {
  205. err = telemetry.Error(ctx, span, err, "error installing pre-deploy job chart")
  206. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "install-pre-deploy-job-error", Value: err})
  207. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  208. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  209. _, uninstallChartErr := helmAgent.UninstallChart(ctx, fmt.Sprintf("%s-r", appName))
  210. if uninstallChartErr != nil {
  211. uninstallChartErr = telemetry.Error(ctx, span, err, "error uninstalling pre-deploy job chart after failed install")
  212. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(uninstallChartErr, http.StatusInternalServerError))
  213. }
  214. return
  215. }
  216. }
  217. conf := &helm.InstallChartConfig{
  218. Chart: chart,
  219. Name: appName,
  220. Namespace: namespace,
  221. Values: values,
  222. Cluster: cluster,
  223. Repo: c.Repo(),
  224. Registries: registries,
  225. }
  226. // create the app chart
  227. release, err := helmAgent.InstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  228. if err != nil {
  229. err = telemetry.Error(ctx, span, err, "error installing app chart")
  230. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  231. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  232. _, err = helmAgent.UninstallChart(ctx, appName)
  233. if err != nil {
  234. err = telemetry.Error(ctx, span, err, "error uninstalling app chart after failed install")
  235. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  236. }
  237. return
  238. }
  239. existing, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
  240. if err != nil {
  241. err = telemetry.Error(ctx, span, err, "error reading app from DB")
  242. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  243. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  244. return
  245. } else if existing.Name != "" {
  246. err = telemetry.Error(ctx, span, err, "app with name already exists in project")
  247. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  248. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
  249. return
  250. }
  251. app := &models.PorterApp{
  252. Name: appName,
  253. ClusterID: cluster.ID,
  254. ProjectID: project.ID,
  255. RepoName: request.RepoName,
  256. GitRepoID: request.GitRepoID,
  257. GitBranch: request.GitBranch,
  258. BuildContext: request.BuildContext,
  259. Builder: request.Builder,
  260. Buildpacks: request.Buildpacks,
  261. Dockerfile: request.Dockerfile,
  262. ImageRepoURI: request.ImageRepoURI,
  263. PullRequestURL: request.PullRequestURL,
  264. PorterYamlPath: request.PorterYamlPath,
  265. }
  266. // create the db entry
  267. porterApp, err := c.Repo().PorterApp().UpdatePorterApp(app)
  268. if err != nil {
  269. err = telemetry.Error(ctx, span, err, "error writing app to DB")
  270. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  271. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  272. return
  273. }
  274. if features.AreAgentDeployEventsEnabled(k8sAgent) {
  275. serviceDeploymentStatusMap := getServiceDeploymentMetadataFromValues(values, types.PorterAppEventStatus_Progressing)
  276. _, err = createNewPorterAppDeployEvent(ctx, serviceDeploymentStatusMap, types.PorterAppEventStatus_Progressing, porterApp.ID, 1, imageInfo.Tag, c.Repo().PorterAppEvent())
  277. } else {
  278. _, err = createOldPorterAppDeployEvent(ctx, types.PorterAppEventStatus_Success, porterApp.ID, 1, imageInfo.Tag, c.Repo().PorterAppEvent())
  279. }
  280. if err != nil {
  281. err = telemetry.Error(ctx, span, err, "error creating porter app event")
  282. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  283. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  284. return
  285. }
  286. c.WriteResult(w, r, porterApp.ToPorterAppTypeWithRevision(release.Version))
  287. } else {
  288. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "upgrading-application", Value: true})
  289. // create/update the pre-deploy job chart
  290. if request.OverrideRelease {
  291. if preDeployJobValues == nil {
  292. preDeployJobName := fmt.Sprintf("%s-r", appName)
  293. _, err := helmAgent.GetRelease(ctx, preDeployJobName, 0, false)
  294. if err == nil {
  295. // handle exception where the user has chosen to delete the release job
  296. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deleting-pre-deploy-job", Value: true})
  297. _, err = helmAgent.UninstallChart(ctx, preDeployJobName)
  298. if err != nil {
  299. err = telemetry.Error(ctx, span, err, "error uninstalling pre-deploy job chart")
  300. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  301. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  302. return
  303. }
  304. }
  305. } else {
  306. preDeployJobName := fmt.Sprintf("%s-r", appName)
  307. helmRelease, err := helmAgent.GetRelease(ctx, preDeployJobName, 0, false)
  308. if err != nil {
  309. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "creating-pre-deploy-job", Value: true})
  310. conf, err := createPreDeployJobChart(
  311. ctx,
  312. appName,
  313. preDeployJobValues,
  314. c.Config().ServerConf.DefaultApplicationHelmRepoURL,
  315. registries,
  316. cluster,
  317. c.Repo(),
  318. )
  319. if err != nil {
  320. err = telemetry.Error(ctx, span, err, "error making config for pre-deploy job chart")
  321. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  322. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  323. return
  324. }
  325. _, err = helmAgent.InstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  326. if err != nil {
  327. err = telemetry.Error(ctx, span, err, "error installing pre-deploy job chart")
  328. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "install-pre-deploy-job-error", Value: err})
  329. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  330. _, uninstallChartErr := helmAgent.UninstallChart(ctx, fmt.Sprintf("%s-r", appName))
  331. if uninstallChartErr != nil {
  332. uninstallChartErr = telemetry.Error(ctx, span, err, "error uninstalling pre-deploy job chart after failed install")
  333. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(uninstallChartErr, http.StatusInternalServerError))
  334. }
  335. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  336. return
  337. }
  338. } else {
  339. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "updating-pre-deploy-job", Value: true})
  340. chart, err := loader.LoadChartPublic(ctx, c.Config().Metadata.DefaultAppHelmRepoURL, "job", "")
  341. if err != nil {
  342. err = telemetry.Error(ctx, span, err, "error loading latest job chart")
  343. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  344. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  345. return
  346. }
  347. conf := &helm.UpgradeReleaseConfig{
  348. Name: helmRelease.Name,
  349. Cluster: cluster,
  350. Repo: c.Repo(),
  351. Registries: registries,
  352. Values: preDeployJobValues,
  353. Chart: chart,
  354. }
  355. _, err = helmAgent.UpgradeReleaseByValues(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection, false)
  356. if err != nil {
  357. err = telemetry.Error(ctx, span, err, "error upgrading pre-deploy job chart")
  358. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  359. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  360. return
  361. }
  362. }
  363. }
  364. }
  365. // update the app chart
  366. conf := &helm.InstallChartConfig{
  367. Chart: chart,
  368. Name: appName,
  369. Namespace: namespace,
  370. Values: values,
  371. Cluster: cluster,
  372. Repo: c.Repo(),
  373. Registries: registries,
  374. }
  375. // update the chart
  376. release, err := helmAgent.UpgradeInstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  377. if err != nil {
  378. err = telemetry.Error(ctx, span, err, "error upgrading application")
  379. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  380. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  381. return
  382. }
  383. // update the DB entry
  384. app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
  385. if err != nil {
  386. err = telemetry.Error(ctx, span, err, "error reading app from DB")
  387. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  388. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  389. return
  390. }
  391. if app == nil {
  392. err = telemetry.Error(ctx, span, nil, "app with name does not exist in project")
  393. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  394. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
  395. return
  396. }
  397. if request.RepoName != "" {
  398. app.RepoName = request.RepoName
  399. }
  400. if request.GitBranch != "" {
  401. app.GitBranch = request.GitBranch
  402. }
  403. if request.BuildContext != "" {
  404. app.BuildContext = request.BuildContext
  405. }
  406. // handles deletion of builder,buildpacks, and dockerfile path
  407. if request.Builder != "" {
  408. if request.Builder == "null" {
  409. app.Builder = ""
  410. } else {
  411. app.Builder = request.Builder
  412. }
  413. }
  414. if request.Buildpacks != "" {
  415. if request.Buildpacks == "null" {
  416. app.Buildpacks = ""
  417. } else {
  418. app.Buildpacks = request.Buildpacks
  419. }
  420. }
  421. if request.Dockerfile != "" {
  422. if request.Dockerfile == "null" {
  423. app.Dockerfile = ""
  424. } else {
  425. app.Dockerfile = request.Dockerfile
  426. }
  427. }
  428. if request.ImageRepoURI != "" {
  429. app.ImageRepoURI = request.ImageRepoURI
  430. }
  431. if request.PullRequestURL != "" {
  432. app.PullRequestURL = request.PullRequestURL
  433. }
  434. telemetry.WithAttributes(
  435. span,
  436. telemetry.AttributeKV{Key: "updated-repo-name", Value: app.RepoName},
  437. telemetry.AttributeKV{Key: "updated-git-branch", Value: app.GitBranch},
  438. telemetry.AttributeKV{Key: "updated-build-context", Value: app.BuildContext},
  439. telemetry.AttributeKV{Key: "updated-builder", Value: app.Builder},
  440. telemetry.AttributeKV{Key: "updated-buildpacks", Value: app.Buildpacks},
  441. telemetry.AttributeKV{Key: "updated-dockerfile", Value: app.Dockerfile},
  442. telemetry.AttributeKV{Key: "updated-image-repo-uri", Value: app.ImageRepoURI},
  443. telemetry.AttributeKV{Key: "updated-pull-request-url", Value: app.PullRequestURL},
  444. )
  445. updatedPorterApp, err := c.Repo().PorterApp().UpdatePorterApp(app)
  446. if err != nil {
  447. err = telemetry.Error(ctx, span, err, "error writing updated app to DB")
  448. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  449. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  450. return
  451. }
  452. if features.AreAgentDeployEventsEnabled(k8sAgent) {
  453. serviceDeploymentStatusMap := getServiceDeploymentMetadataFromValues(values, types.PorterAppEventStatus_Progressing)
  454. _, err = createNewPorterAppDeployEvent(ctx, serviceDeploymentStatusMap, types.PorterAppEventStatus_Progressing, updatedPorterApp.ID, helmRelease.Version+1, imageInfo.Tag, c.Repo().PorterAppEvent())
  455. } else {
  456. _, err = createOldPorterAppDeployEvent(ctx, types.PorterAppEventStatus_Success, updatedPorterApp.ID, helmRelease.Version+1, imageInfo.Tag, c.Repo().PorterAppEvent())
  457. }
  458. if err != nil {
  459. err = telemetry.Error(ctx, span, err, "error creating porter app event")
  460. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  461. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  462. return
  463. }
  464. c.WriteResult(w, r, updatedPorterApp.ToPorterAppTypeWithRevision(release.Version))
  465. }
  466. }
  467. // createOldPorterAppDeployEvent creates an event for use in the activity feed
  468. // TODO: remove this method and all call-sites if this span no longer exists in telemetry for 4 consecutive weeks
  469. func createOldPorterAppDeployEvent(ctx context.Context, status types.PorterAppEventStatus, appID uint, revision int, tag string, repo repository.PorterAppEventRepository) (*models.PorterAppEvent, error) {
  470. ctx, span := telemetry.NewSpan(ctx, "create-old-porter-app-deploy-event")
  471. defer span.End()
  472. event := models.PorterAppEvent{
  473. ID: uuid.New(),
  474. Status: string(status),
  475. Type: "DEPLOY",
  476. TypeExternalSource: "KUBERNETES",
  477. PorterAppID: appID,
  478. Metadata: map[string]any{
  479. "revision": revision,
  480. "image_tag": tag,
  481. },
  482. }
  483. err := repo.CreateEvent(ctx, &event)
  484. if err != nil {
  485. return nil, err
  486. }
  487. if event.ID == uuid.Nil {
  488. return nil, err
  489. }
  490. return &event, nil
  491. }
  492. // createNewPorterAppDeployEvent creates an event for use in the activity feed, supplemented with information about the
  493. // deployed services in serviceStatusMap as well as the image tag being deployed
  494. func createNewPorterAppDeployEvent(
  495. ctx context.Context,
  496. serviceStatusMap map[string]types.ServiceDeploymentMetadata,
  497. status types.PorterAppEventStatus,
  498. appID uint,
  499. revision int,
  500. tag string,
  501. repo repository.PorterAppEventRepository,
  502. ) (*models.PorterAppEvent, error) {
  503. ctx, span := telemetry.NewSpan(ctx, "create-new-porter-app-deploy-event")
  504. defer span.End()
  505. // mark all pending deployments from the deploy event of the previous revision as canceled
  506. updatePreviousPorterAppDeployEvent(ctx, appID, revision, repo)
  507. event := models.PorterAppEvent{
  508. ID: uuid.New(),
  509. Status: string(status),
  510. Type: "DEPLOY",
  511. TypeExternalSource: "KUBERNETES",
  512. PorterAppID: appID,
  513. Metadata: map[string]any{
  514. "revision": revision,
  515. "image_tag": tag,
  516. "service_deployment_metadata": serviceStatusMap,
  517. },
  518. }
  519. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "revision", Value: revision}, telemetry.AttributeKV{Key: "image-tag", Value: tag})
  520. err := repo.CreateEvent(ctx, &event)
  521. if err != nil {
  522. err = telemetry.Error(ctx, span, err, "error creating porter app event")
  523. return nil, err
  524. }
  525. if event.ID == uuid.Nil {
  526. return nil, telemetry.Error(ctx, span, nil, "event id for newly created app event is nil")
  527. }
  528. return &event, nil
  529. }
  530. // updatePreviousPorterAppDeployEvent updates the previous deploy event to change the event status as well as all service statuses to CANCELED
  531. // if it is still in the PROGRESSING state. This is done to prevent the activity feed from showing an old deploy event as still in progress.
  532. func updatePreviousPorterAppDeployEvent(ctx context.Context, appID uint, revision int, repo repository.PorterAppEventRepository) {
  533. ctx, span := telemetry.NewSpan(ctx, "update-previous-porter-app-deploy-event")
  534. defer span.End()
  535. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "updating-previous-event", Value: false}, telemetry.AttributeKV{Key: "new-revision", Value: revision})
  536. if revision <= 1 {
  537. return
  538. }
  539. revisionFloat64 := float64(revision - 1)
  540. matchEvent, err := repo.ReadDeployEventByRevision(ctx, appID, revisionFloat64)
  541. if err != nil {
  542. _ = telemetry.Error(ctx, span, err, "error reading deploy event by revision")
  543. return
  544. }
  545. if matchEvent.ID == uuid.Nil {
  546. _ = telemetry.Error(ctx, span, nil, "could not find previous deploy event")
  547. return
  548. }
  549. if matchEvent.Status != string(types.PorterAppEventStatus_Progressing) {
  550. return
  551. }
  552. serviceStatus, ok := matchEvent.Metadata["service_deployment_metadata"]
  553. if !ok {
  554. _ = telemetry.Error(ctx, span, nil, "service deployment metadata not found in deploy event metadata")
  555. return
  556. }
  557. serviceDeploymentGenericMap, ok := serviceStatus.(map[string]interface{})
  558. if !ok {
  559. _ = telemetry.Error(ctx, span, nil, "service deployment metadata is not map[string]interface{}")
  560. return
  561. }
  562. serviceDeploymentMap := make(map[string]types.ServiceDeploymentMetadata)
  563. for k, v := range serviceDeploymentGenericMap {
  564. by, err := json.Marshal(v)
  565. if err != nil {
  566. _ = telemetry.Error(ctx, span, nil, "unable to marshal")
  567. return
  568. }
  569. var serviceDeploymentMetadata types.ServiceDeploymentMetadata
  570. err = json.Unmarshal(by, &serviceDeploymentMetadata)
  571. if err != nil {
  572. _ = telemetry.Error(ctx, span, nil, "unable to unmarshal")
  573. return
  574. }
  575. serviceDeploymentMap[k] = serviceDeploymentMetadata
  576. }
  577. for key, serviceDeploymentMetadata := range serviceDeploymentMap {
  578. if serviceDeploymentMetadata.Status == types.PorterAppEventStatus_Progressing {
  579. serviceDeploymentMetadata.Status = types.PorterAppEventStatus_Canceled
  580. serviceDeploymentMap[key] = serviceDeploymentMetadata
  581. }
  582. }
  583. matchEvent.Metadata["service_deployment_metadata"] = serviceDeploymentMap
  584. matchEvent.Status = string(types.PorterAppEventStatus_Canceled)
  585. err = repo.UpdateEvent(ctx, &matchEvent)
  586. if err != nil {
  587. _ = telemetry.Error(ctx, span, err, "error updating deploy event")
  588. return
  589. }
  590. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "updating-previous-event", Value: true})
  591. }
  592. func createPreDeployJobChart(
  593. ctx context.Context,
  594. stackName string,
  595. values map[string]interface{},
  596. repoUrl string,
  597. registries []*models.Registry,
  598. cluster *models.Cluster,
  599. repo repository.Repository,
  600. ) (*helm.InstallChartConfig, error) {
  601. chart, err := loader.LoadChartPublic(ctx, repoUrl, "job", "")
  602. if err != nil {
  603. return nil, err
  604. }
  605. releaseName := utils.PredeployJobNameFromPorterAppName(stackName)
  606. namespace := utils.NamespaceFromPorterAppName(stackName)
  607. return &helm.InstallChartConfig{
  608. Chart: chart,
  609. Name: releaseName,
  610. Namespace: namespace,
  611. Values: values,
  612. Cluster: cluster,
  613. Repo: repo,
  614. Registries: registries,
  615. }, nil
  616. }
  617. func cloneEnvGroup(c *CreatePorterAppHandler, w http.ResponseWriter, r *http.Request, agent *kubernetes.Agent, envGroups []string, namespace string) {
  618. for _, envGroupName := range envGroups {
  619. cm, _, err := agent.GetLatestVersionedConfigMap(envGroupName, "porter-env-group")
  620. if err != nil {
  621. if errors.Is(err, kubernetes.IsNotFoundError) {
  622. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  623. fmt.Errorf("error cloning env group: envgroup %s in namespace %s not found", envGroupName, "porter-env-group"), http.StatusNotFound,
  624. "no config map found for envgroup",
  625. ))
  626. return
  627. }
  628. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  629. return
  630. }
  631. secret, _, err := agent.GetLatestVersionedSecret(envGroupName, "porter-env-group")
  632. if err != nil {
  633. if errors.Is(err, kubernetes.IsNotFoundError) {
  634. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  635. fmt.Errorf("error cloning env group: envgroup %s in namespace %s not found", envGroupName, "porter-env-group"), http.StatusNotFound,
  636. "no k8s secret found for envgroup",
  637. ))
  638. return
  639. }
  640. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  641. return
  642. }
  643. vars := make(map[string]string)
  644. secretVars := make(map[string]string)
  645. for key, val := range cm.Data {
  646. if !strings.Contains(val, "PORTERSECRET") {
  647. vars[key] = val
  648. }
  649. }
  650. for key, val := range secret.Data {
  651. secretVars[key] = string(val)
  652. }
  653. configMap, err := envgroup.CreateEnvGroup(agent, types.ConfigMapInput{
  654. Name: envGroupName,
  655. Namespace: namespace,
  656. Variables: vars,
  657. SecretVariables: secretVars,
  658. })
  659. if err != nil {
  660. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  661. return
  662. }
  663. _, err = envgroup.ToEnvGroup(configMap)
  664. if err != nil {
  665. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  666. return
  667. }
  668. }
  669. }