create.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. package porter_app
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "fmt"
  6. "net/http"
  7. "strings"
  8. "github.com/google/uuid"
  9. "github.com/porter-dev/porter/internal/telemetry"
  10. "github.com/porter-dev/porter/api/server/authz"
  11. "github.com/porter-dev/porter/api/server/handlers"
  12. "github.com/porter-dev/porter/api/server/shared"
  13. "github.com/porter-dev/porter/api/server/shared/apierrors"
  14. "github.com/porter-dev/porter/api/server/shared/config"
  15. "github.com/porter-dev/porter/api/server/shared/requestutils"
  16. "github.com/porter-dev/porter/api/types"
  17. "github.com/porter-dev/porter/internal/helm"
  18. "github.com/porter-dev/porter/internal/helm/loader"
  19. "github.com/porter-dev/porter/internal/models"
  20. "github.com/porter-dev/porter/internal/repository"
  21. "github.com/stefanmcshane/helm/pkg/chart"
  22. )
  23. type CreatePorterAppHandler struct {
  24. handlers.PorterHandlerReadWriter
  25. authz.KubernetesAgentGetter
  26. }
  27. func NewCreatePorterAppHandler(
  28. config *config.Config,
  29. decoderValidator shared.RequestDecoderValidator,
  30. writer shared.ResultWriter,
  31. ) *CreatePorterAppHandler {
  32. return &CreatePorterAppHandler{
  33. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  34. KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
  35. }
  36. }
  37. func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  38. ctx := r.Context()
  39. project, _ := ctx.Value(types.ProjectScope).(*models.Project)
  40. cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
  41. ctx, span := telemetry.NewSpan(r.Context(), "serve-create-porter-app")
  42. defer span.End()
  43. request := &types.CreatePorterAppRequest{}
  44. if ok := c.DecodeAndValidate(w, r, request); !ok {
  45. err := telemetry.Error(ctx, span, nil, "error decoding request")
  46. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  47. return
  48. }
  49. stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
  50. if reqErr != nil {
  51. err := telemetry.Error(ctx, span, reqErr, "error getting stack name from url")
  52. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  53. return
  54. }
  55. namespace := fmt.Sprintf("porter-stack-%s", stackName)
  56. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "application-name", Value: stackName})
  57. helmAgent, err := c.GetHelmAgent(ctx, r, cluster, namespace)
  58. if err != nil {
  59. err = telemetry.Error(ctx, span, err, "error getting helm agent")
  60. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  61. return
  62. }
  63. k8sAgent, err := c.GetAgent(r, cluster, namespace)
  64. if err != nil {
  65. err = telemetry.Error(ctx, span, err, "error getting k8s agent")
  66. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  67. return
  68. }
  69. helmRelease, err := helmAgent.GetRelease(ctx, stackName, 0, false)
  70. shouldCreate := err != nil
  71. porterYamlBase64 := request.PorterYAMLBase64
  72. porterYaml, err := base64.StdEncoding.DecodeString(porterYamlBase64)
  73. if err != nil {
  74. err = telemetry.Error(ctx, span, err, "error decoding porter yaml")
  75. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  76. return
  77. }
  78. imageInfo := request.ImageInfo
  79. registries, err := c.Repo().Registry().ListRegistriesByProjectID(cluster.ProjectID)
  80. if err != nil {
  81. err = telemetry.Error(ctx, span, err, "error listing registries")
  82. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  83. return
  84. }
  85. var releaseValues map[string]interface{}
  86. var releaseDependencies []*chart.Dependency
  87. if shouldCreate || request.OverrideRelease {
  88. releaseValues = nil
  89. releaseDependencies = nil
  90. // this is required because when the front-end sends an update request with overrideRelease=true, it is unable to
  91. // get the image info from the release. unless it is explicitly provided in the request, we avoid overwriting it
  92. // by attempting to get the image info from the release
  93. if helmRelease != nil && (imageInfo.Repository == "" || imageInfo.Tag == "") {
  94. imageInfo = attemptToGetImageInfoFromRelease(helmRelease.Config)
  95. }
  96. } else {
  97. releaseValues = helmRelease.Config
  98. releaseDependencies = helmRelease.Chart.Metadata.Dependencies
  99. }
  100. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "image-repo", Value: imageInfo.Repository}, telemetry.AttributeKV{Key: "image-tag", Value: imageInfo.Tag})
  101. if request.Builder == "" {
  102. // attempt to get builder from db
  103. app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
  104. if err == nil {
  105. request.Builder = app.Builder
  106. }
  107. }
  108. injectLauncher := strings.Contains(request.Builder, "heroku") ||
  109. strings.Contains(request.Builder, "paketo")
  110. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "builder", Value: request.Builder})
  111. chart, values, releaseJobValues, err := parse(
  112. porterYaml,
  113. imageInfo,
  114. c.Config(),
  115. cluster.ProjectID,
  116. releaseValues,
  117. releaseDependencies,
  118. SubdomainCreateOpts{
  119. k8sAgent: k8sAgent,
  120. dnsRepo: c.Repo().DNSRecord(),
  121. powerDnsClient: c.Config().PowerDNSClient,
  122. appRootDomain: c.Config().ServerConf.AppRootDomain,
  123. stackName: stackName,
  124. },
  125. injectLauncher,
  126. )
  127. if err != nil {
  128. err = telemetry.Error(ctx, span, err, "error parsing porter yaml into chart and values")
  129. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  130. return
  131. }
  132. if shouldCreate {
  133. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "installing-application", Value: true})
  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. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  139. return
  140. }
  141. // create the release job chart if it does not exist (only done by front-end currently, where we set overrideRelease=true)
  142. if request.OverrideRelease && releaseJobValues != nil {
  143. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "installing-pre-deploy-job", Value: true})
  144. conf, err := createReleaseJobChart(
  145. ctx,
  146. stackName,
  147. releaseJobValues,
  148. c.Config().ServerConf.DefaultApplicationHelmRepoURL,
  149. registries,
  150. cluster,
  151. c.Repo(),
  152. )
  153. if err != nil {
  154. err = telemetry.Error(ctx, span, err, "error making config for pre-deploy job chart")
  155. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  156. return
  157. }
  158. _, err = helmAgent.InstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  159. if err != nil {
  160. err = telemetry.Error(ctx, span, err, "error installing pre-deploy job chart")
  161. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "install-pre-deploy-job-error", Value: err})
  162. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  163. _, uninstallChartErr := helmAgent.UninstallChart(ctx, fmt.Sprintf("%s-r", stackName))
  164. if uninstallChartErr != nil {
  165. uninstallChartErr = telemetry.Error(ctx, span, err, "error uninstalling pre-deploy job chart after failed install")
  166. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(uninstallChartErr, http.StatusInternalServerError))
  167. }
  168. return
  169. }
  170. }
  171. conf := &helm.InstallChartConfig{
  172. Chart: chart,
  173. Name: stackName,
  174. Namespace: namespace,
  175. Values: values,
  176. Cluster: cluster,
  177. Repo: c.Repo(),
  178. Registries: registries,
  179. }
  180. // create the app chart
  181. _, err = helmAgent.InstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  182. if err != nil {
  183. err = telemetry.Error(ctx, span, err, "error installing app chart")
  184. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  185. _, err = helmAgent.UninstallChart(ctx, stackName)
  186. if err != nil {
  187. err = telemetry.Error(ctx, span, err, "error uninstalling app chart after failed install")
  188. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  189. }
  190. return
  191. }
  192. existing, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
  193. if err != nil {
  194. err = telemetry.Error(ctx, span, err, "error reading app from DB")
  195. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  196. return
  197. } else if existing.Name != "" {
  198. err = telemetry.Error(ctx, span, err, "app with name already exists in project")
  199. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
  200. return
  201. }
  202. app := &models.PorterApp{
  203. Name: stackName,
  204. ClusterID: cluster.ID,
  205. ProjectID: project.ID,
  206. RepoName: request.RepoName,
  207. GitRepoID: request.GitRepoID,
  208. GitBranch: request.GitBranch,
  209. BuildContext: request.BuildContext,
  210. Builder: request.Builder,
  211. Buildpacks: request.Buildpacks,
  212. Dockerfile: request.Dockerfile,
  213. ImageRepoURI: request.ImageRepoURI,
  214. PullRequestURL: request.PullRequestURL,
  215. PorterYamlPath: request.PorterYamlPath,
  216. }
  217. // create the db entry
  218. porterApp, err := c.Repo().PorterApp().UpdatePorterApp(app)
  219. if err != nil {
  220. err = telemetry.Error(ctx, span, err, "error writing app to DB")
  221. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  222. return
  223. }
  224. _, err = createPorterAppEvent(ctx, "SUCCESS", porterApp.ID, 1, imageInfo.Tag, c.Repo().PorterAppEvent())
  225. if err != nil {
  226. err = telemetry.Error(ctx, span, err, "error creating porter app event")
  227. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  228. return
  229. }
  230. c.WriteResult(w, r, porterApp.ToPorterAppType())
  231. } else {
  232. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "upgrading-application", Value: true})
  233. // create/update the release job chart
  234. if request.OverrideRelease {
  235. if releaseJobValues == nil {
  236. releaseJobName := fmt.Sprintf("%s-r", stackName)
  237. _, err := helmAgent.GetRelease(ctx, releaseJobName, 0, false)
  238. if err == nil {
  239. // handle exception where the user has chosen to delete the release job
  240. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deleting-pre-deploy-job", Value: true})
  241. _, err = helmAgent.UninstallChart(ctx, releaseJobName)
  242. if err != nil {
  243. err = telemetry.Error(ctx, span, err, "error uninstalling pre-deploy job chart")
  244. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  245. return
  246. }
  247. }
  248. } else {
  249. releaseJobName := fmt.Sprintf("%s-r", stackName)
  250. helmRelease, err := helmAgent.GetRelease(ctx, releaseJobName, 0, false)
  251. if err != nil {
  252. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "creating-pre-deploy-job", Value: true})
  253. conf, err := createReleaseJobChart(
  254. ctx,
  255. stackName,
  256. releaseJobValues,
  257. c.Config().ServerConf.DefaultApplicationHelmRepoURL,
  258. registries,
  259. cluster,
  260. c.Repo(),
  261. )
  262. if err != nil {
  263. err = telemetry.Error(ctx, span, err, "error making config for pre-deploy job chart")
  264. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  265. return
  266. }
  267. _, err = helmAgent.InstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  268. if err != nil {
  269. err = telemetry.Error(ctx, span, err, "error installing pre-deploy job chart")
  270. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "install-pre-deploy-job-error", Value: err})
  271. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  272. _, uninstallChartErr := helmAgent.UninstallChart(ctx, fmt.Sprintf("%s-r", stackName))
  273. if uninstallChartErr != nil {
  274. uninstallChartErr = telemetry.Error(ctx, span, err, "error uninstalling pre-deploy job chart after failed install")
  275. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(uninstallChartErr, http.StatusInternalServerError))
  276. }
  277. return
  278. }
  279. } else {
  280. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "updating-pre-deploy-job", Value: true})
  281. chart, err := loader.LoadChartPublic(ctx, c.Config().Metadata.DefaultAppHelmRepoURL, "job", "")
  282. if err != nil {
  283. err = telemetry.Error(ctx, span, err, "error loading latest job chart")
  284. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  285. return
  286. }
  287. conf := &helm.UpgradeReleaseConfig{
  288. Name: helmRelease.Name,
  289. Cluster: cluster,
  290. Repo: c.Repo(),
  291. Registries: registries,
  292. Values: releaseJobValues,
  293. Chart: chart,
  294. }
  295. _, err = helmAgent.UpgradeReleaseByValues(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection, false)
  296. if err != nil {
  297. err = telemetry.Error(ctx, span, err, "error upgrading pre-deploy job chart")
  298. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  299. return
  300. }
  301. }
  302. }
  303. }
  304. // update the app chart
  305. conf := &helm.InstallChartConfig{
  306. Chart: chart,
  307. Name: stackName,
  308. Namespace: namespace,
  309. Values: values,
  310. Cluster: cluster,
  311. Repo: c.Repo(),
  312. Registries: registries,
  313. }
  314. // update the chart
  315. _, err = helmAgent.UpgradeInstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  316. if err != nil {
  317. err = telemetry.Error(ctx, span, err, "error upgrading application")
  318. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  319. return
  320. }
  321. // update the DB entry
  322. app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
  323. if err != nil {
  324. err = telemetry.Error(ctx, span, err, "error reading app from DB")
  325. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  326. return
  327. }
  328. if request.RepoName != "" {
  329. app.RepoName = request.RepoName
  330. }
  331. if request.GitBranch != "" {
  332. app.GitBranch = request.GitBranch
  333. }
  334. if request.BuildContext != "" {
  335. app.BuildContext = request.BuildContext
  336. }
  337. if request.Builder != "" {
  338. if request.Builder == "null" {
  339. app.Builder = ""
  340. } else {
  341. app.Builder = request.Builder
  342. }
  343. }
  344. if request.Buildpacks != "" {
  345. if request.Buildpacks == "null" {
  346. app.Buildpacks = ""
  347. } else {
  348. app.Buildpacks = request.Buildpacks
  349. }
  350. }
  351. if request.Dockerfile != "" {
  352. if request.Dockerfile == "null" {
  353. app.Dockerfile = ""
  354. } else {
  355. app.Dockerfile = request.Dockerfile
  356. }
  357. }
  358. if request.ImageRepoURI != "" {
  359. app.ImageRepoURI = request.ImageRepoURI
  360. }
  361. if request.PullRequestURL != "" {
  362. app.PullRequestURL = request.PullRequestURL
  363. }
  364. telemetry.WithAttributes(
  365. span,
  366. telemetry.AttributeKV{Key: "updated-repo-name", Value: app.RepoName},
  367. telemetry.AttributeKV{Key: "updated-git-branch", Value: app.GitBranch},
  368. telemetry.AttributeKV{Key: "updated-build-context", Value: app.BuildContext},
  369. telemetry.AttributeKV{Key: "updated-builder", Value: app.Builder},
  370. telemetry.AttributeKV{Key: "updated-buildpacks", Value: app.Buildpacks},
  371. telemetry.AttributeKV{Key: "updated-dockerfile", Value: app.Dockerfile},
  372. telemetry.AttributeKV{Key: "updated-image-repo-uri", Value: app.ImageRepoURI},
  373. telemetry.AttributeKV{Key: "updated-pull-request-url", Value: app.PullRequestURL},
  374. )
  375. updatedPorterApp, err := c.Repo().PorterApp().UpdatePorterApp(app)
  376. if err != nil {
  377. err = telemetry.Error(ctx, span, err, "error writing updated app to DB")
  378. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  379. return
  380. }
  381. _, err = createPorterAppEvent(ctx, "SUCCESS", updatedPorterApp.ID, helmRelease.Version+1, imageInfo.Tag, c.Repo().PorterAppEvent())
  382. if err != nil {
  383. err = telemetry.Error(ctx, span, err, "error creating porter app event")
  384. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  385. return
  386. }
  387. c.WriteResult(w, r, updatedPorterApp.ToPorterAppType())
  388. }
  389. }
  390. // createPorterAppEvent creates an event for use in the activity feed
  391. func createPorterAppEvent(ctx context.Context, status string, appID uint, revision int, tag string, repo repository.PorterAppEventRepository) (*models.PorterAppEvent, error) {
  392. event := models.PorterAppEvent{
  393. ID: uuid.New(),
  394. Status: status,
  395. Type: "DEPLOY",
  396. TypeExternalSource: "KUBERNETES",
  397. PorterAppID: appID,
  398. Metadata: map[string]any{
  399. "revision": revision,
  400. "image_tag": tag,
  401. },
  402. }
  403. err := repo.CreateEvent(ctx, &event)
  404. if err != nil {
  405. return nil, err
  406. }
  407. if event.ID == uuid.Nil {
  408. return nil, err
  409. }
  410. return &event, nil
  411. }
  412. func createReleaseJobChart(
  413. ctx context.Context,
  414. stackName string,
  415. values map[string]interface{},
  416. repoUrl string,
  417. registries []*models.Registry,
  418. cluster *models.Cluster,
  419. repo repository.Repository,
  420. ) (*helm.InstallChartConfig, error) {
  421. chart, err := loader.LoadChartPublic(ctx, repoUrl, "job", "")
  422. if err != nil {
  423. return nil, err
  424. }
  425. releaseName := fmt.Sprintf("%s-r", stackName)
  426. namespace := fmt.Sprintf("porter-stack-%s", stackName)
  427. return &helm.InstallChartConfig{
  428. Chart: chart,
  429. Name: releaseName,
  430. Namespace: namespace,
  431. Values: values,
  432. Cluster: cluster,
  433. Repo: repo,
  434. Registries: registries,
  435. }, nil
  436. }