create.go 18 KB

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