create.go 29 KB

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