create.go 23 KB

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