create.go 29 KB

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