create.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  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. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error decoding request")))
  46. return
  47. }
  48. stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
  49. if reqErr != nil {
  50. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
  51. return
  52. }
  53. namespace := fmt.Sprintf("porter-stack-%s", stackName)
  54. helmAgent, err := c.GetHelmAgent(ctx, r, cluster, namespace)
  55. if err != nil {
  56. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error getting helm agent: %w", err)))
  57. return
  58. }
  59. k8sAgent, err := c.GetAgent(r, cluster, namespace)
  60. if err != nil {
  61. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error getting k8s agent: %w", err)))
  62. return
  63. }
  64. helmRelease, err := helmAgent.GetRelease(ctx, stackName, 0, false)
  65. shouldCreate := err != nil
  66. porterYamlBase64 := request.PorterYAMLBase64
  67. porterYaml, err := base64.StdEncoding.DecodeString(porterYamlBase64)
  68. if err != nil {
  69. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error decoding porter.yaml: %w", err)))
  70. return
  71. }
  72. imageInfo := request.ImageInfo
  73. registries, err := c.Repo().Registry().ListRegistriesByProjectID(cluster.ProjectID)
  74. if err != nil {
  75. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error listing registries: %w", err)))
  76. return
  77. }
  78. var releaseValues map[string]interface{}
  79. var releaseDependencies []*chart.Dependency
  80. if shouldCreate || request.OverrideRelease {
  81. releaseValues = nil
  82. releaseDependencies = nil
  83. // this is required because when the front-end sends an update request with overrideRelease=true, it is unable to
  84. // get the image info from the release. unless it is explicitly provided in the request, we avoid overwriting it
  85. // by attempting to get the image info from the release
  86. if helmRelease != nil && (imageInfo.Repository == "" || imageInfo.Tag == "") {
  87. imageInfo = attemptToGetImageInfoFromRelease(helmRelease.Config)
  88. }
  89. } else {
  90. releaseValues = helmRelease.Config
  91. releaseDependencies = helmRelease.Chart.Metadata.Dependencies
  92. }
  93. if request.Builder == "" {
  94. // attempt to get builder from db
  95. app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
  96. if err == nil {
  97. request.Builder = app.Builder
  98. }
  99. }
  100. injectLauncher := strings.Contains(request.Builder, "heroku") ||
  101. strings.Contains(request.Builder, "paketo")
  102. chart, values, releaseJobValues, err := parse(
  103. porterYaml,
  104. imageInfo,
  105. c.Config(),
  106. cluster.ProjectID,
  107. releaseValues,
  108. releaseDependencies,
  109. SubdomainCreateOpts{
  110. k8sAgent: k8sAgent,
  111. dnsRepo: c.Repo().DNSRecord(),
  112. powerDnsClient: c.Config().PowerDNSClient,
  113. appRootDomain: c.Config().ServerConf.AppRootDomain,
  114. stackName: stackName,
  115. },
  116. injectLauncher,
  117. )
  118. if err != nil {
  119. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error parsing porter.yaml into chart and values: %w", err)))
  120. return
  121. }
  122. if shouldCreate {
  123. // create the namespace if it does not exist already
  124. _, err = k8sAgent.CreateNamespace(namespace, nil)
  125. if err != nil {
  126. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating namespace: %w", err)))
  127. return
  128. }
  129. // create the release job chart if it does not exist (only done by front-end currently, where we set overrideRelease=true)
  130. if request.OverrideRelease && releaseJobValues != nil {
  131. conf, err := createReleaseJobChart(
  132. ctx,
  133. stackName,
  134. releaseJobValues,
  135. c.Config().ServerConf.DefaultApplicationHelmRepoURL,
  136. registries,
  137. cluster,
  138. c.Repo(),
  139. )
  140. if err != nil {
  141. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error making config for release job chart: %w", err)))
  142. return
  143. }
  144. _, err = helmAgent.InstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  145. if err != nil {
  146. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating release job chart: %w", err)))
  147. _, err = helmAgent.UninstallChart(ctx, fmt.Sprintf("%s-r", stackName))
  148. if err != nil {
  149. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error uninstalling release job chart: %w", err)))
  150. }
  151. return
  152. }
  153. }
  154. conf := &helm.InstallChartConfig{
  155. Chart: chart,
  156. Name: stackName,
  157. Namespace: namespace,
  158. Values: values,
  159. Cluster: cluster,
  160. Repo: c.Repo(),
  161. Registries: registries,
  162. }
  163. // create the app chart
  164. _, err = helmAgent.InstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  165. if err != nil {
  166. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("error deploying app: %s", err.Error()), http.StatusBadRequest))
  167. _, err = helmAgent.UninstallChart(ctx, stackName)
  168. if err != nil {
  169. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error uninstalling chart: %w", err)))
  170. }
  171. return
  172. }
  173. existing, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
  174. if err != nil {
  175. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  176. return
  177. } else if existing.Name != "" {
  178. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  179. fmt.Errorf("app with name %s already exists in your project", existing.Name), http.StatusForbidden))
  180. return
  181. }
  182. app := &models.PorterApp{
  183. Name: stackName,
  184. ClusterID: cluster.ID,
  185. ProjectID: project.ID,
  186. RepoName: request.RepoName,
  187. GitRepoID: request.GitRepoID,
  188. GitBranch: request.GitBranch,
  189. BuildContext: request.BuildContext,
  190. Builder: request.Builder,
  191. Buildpacks: request.Buildpacks,
  192. Dockerfile: request.Dockerfile,
  193. ImageRepoURI: request.ImageRepoURI,
  194. PullRequestURL: request.PullRequestURL,
  195. PorterYamlPath: request.PorterYamlPath,
  196. }
  197. // create the db entry
  198. porterApp, err := c.Repo().PorterApp().UpdatePorterApp(app)
  199. if err != nil {
  200. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error writing app to DB: %s", err.Error())))
  201. return
  202. }
  203. _, err = createPorterAppEvent(ctx, "SUCCESS", porterApp.ID, 1, imageInfo.Tag, c.Repo().PorterAppEvent())
  204. if err != nil {
  205. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating porter app event: %s", err.Error())))
  206. return
  207. }
  208. c.WriteResult(w, r, porterApp.ToPorterAppType())
  209. } else {
  210. // create/update the release job chart
  211. if request.OverrideRelease {
  212. if releaseJobValues == nil {
  213. // handle exception where the user has chosen to delete the release job
  214. releaseJobName := fmt.Sprintf("%s-r", stackName)
  215. _, err := helmAgent.GetRelease(ctx, releaseJobName, 0, false)
  216. if err == nil {
  217. _, err = helmAgent.UninstallChart(ctx, releaseJobName)
  218. if err != nil {
  219. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error uninstalling release job chart: %w", err)))
  220. return
  221. }
  222. }
  223. } else {
  224. releaseJobName := fmt.Sprintf("%s-r", stackName)
  225. helmRelease, err := helmAgent.GetRelease(ctx, releaseJobName, 0, false)
  226. if err != nil {
  227. conf, err := createReleaseJobChart(
  228. ctx,
  229. stackName,
  230. releaseJobValues,
  231. c.Config().ServerConf.DefaultApplicationHelmRepoURL,
  232. registries,
  233. cluster,
  234. c.Repo(),
  235. )
  236. if err != nil {
  237. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error making config for release job chart: %w", err)))
  238. return
  239. }
  240. _, err = helmAgent.InstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  241. if err != nil {
  242. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating release job chart: %w", err)))
  243. _, err = helmAgent.UninstallChart(ctx, fmt.Sprintf("%s-r", stackName))
  244. if err != nil {
  245. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error uninstalling release job chart: %w", err)))
  246. }
  247. return
  248. }
  249. } else {
  250. chart, err := loader.LoadChartPublic(ctx, c.Config().Metadata.DefaultAppHelmRepoURL, "job", "")
  251. if err != nil {
  252. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("error loading release job chart: %w", err), http.StatusBadRequest))
  253. return
  254. }
  255. conf := &helm.UpgradeReleaseConfig{
  256. Name: helmRelease.Name,
  257. Cluster: cluster,
  258. Repo: c.Repo(),
  259. Registries: registries,
  260. Values: releaseJobValues,
  261. Chart: chart,
  262. }
  263. _, err = helmAgent.UpgradeReleaseByValues(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection, false)
  264. if err != nil {
  265. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error upgrading release job chart: %w", err)))
  266. return
  267. }
  268. }
  269. }
  270. }
  271. // update the app chart
  272. conf := &helm.InstallChartConfig{
  273. Chart: chart,
  274. Name: stackName,
  275. Namespace: namespace,
  276. Values: values,
  277. Cluster: cluster,
  278. Repo: c.Repo(),
  279. Registries: registries,
  280. }
  281. // update the chart
  282. _, err = helmAgent.UpgradeInstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  283. if err != nil {
  284. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("error deploying app: %s", err.Error()), http.StatusBadRequest))
  285. return
  286. }
  287. // update the DB entry
  288. app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
  289. if err != nil {
  290. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  291. return
  292. }
  293. if request.RepoName != "" {
  294. app.RepoName = request.RepoName
  295. }
  296. if request.GitBranch != "" {
  297. app.GitBranch = request.GitBranch
  298. }
  299. if request.BuildContext != "" {
  300. app.BuildContext = request.BuildContext
  301. }
  302. if request.Builder != "" {
  303. if request.Builder == "null" {
  304. app.Builder = ""
  305. } else {
  306. app.Builder = request.Builder
  307. }
  308. }
  309. if request.Buildpacks != "" {
  310. if request.Buildpacks == "null" {
  311. app.Buildpacks = ""
  312. } else {
  313. app.Buildpacks = request.Buildpacks
  314. }
  315. }
  316. if request.Dockerfile != "" {
  317. if request.Dockerfile == "null" {
  318. app.Dockerfile = ""
  319. } else {
  320. app.Dockerfile = request.Dockerfile
  321. }
  322. }
  323. if request.ImageRepoURI != "" {
  324. app.ImageRepoURI = request.ImageRepoURI
  325. }
  326. if request.PullRequestURL != "" {
  327. app.PullRequestURL = request.PullRequestURL
  328. }
  329. updatedPorterApp, err := c.Repo().PorterApp().UpdatePorterApp(app)
  330. if err != nil {
  331. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error writing updated app to DB: %s", err.Error())))
  332. return
  333. }
  334. _, err = createPorterAppEvent(ctx, "SUCCESS", updatedPorterApp.ID, helmRelease.Version+1, imageInfo.Tag, c.Repo().PorterAppEvent())
  335. if err != nil {
  336. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating porter app event: %s", err.Error())))
  337. return
  338. }
  339. c.WriteResult(w, r, updatedPorterApp.ToPorterAppType())
  340. }
  341. }
  342. // createPorterAppEvent creates an event for use in the activity feed
  343. func createPorterAppEvent(ctx context.Context, status string, appID uint, revision int, tag string, repo repository.PorterAppEventRepository) (*models.PorterAppEvent, error) {
  344. event := models.PorterAppEvent{
  345. ID: uuid.New(),
  346. Status: status,
  347. Type: "DEPLOY",
  348. TypeExternalSource: "KUBERNETES",
  349. PorterAppID: appID,
  350. Metadata: map[string]any{
  351. "revision": revision,
  352. "image_tag": tag,
  353. },
  354. }
  355. err := repo.CreateEvent(ctx, &event)
  356. if err != nil {
  357. return nil, err
  358. }
  359. if event.ID == uuid.Nil {
  360. return nil, err
  361. }
  362. return &event, nil
  363. }
  364. func createReleaseJobChart(
  365. ctx context.Context,
  366. stackName string,
  367. values map[string]interface{},
  368. repoUrl string,
  369. registries []*models.Registry,
  370. cluster *models.Cluster,
  371. repo repository.Repository,
  372. ) (*helm.InstallChartConfig, error) {
  373. chart, err := loader.LoadChartPublic(ctx, repoUrl, "job", "")
  374. if err != nil {
  375. return nil, err
  376. }
  377. releaseName := fmt.Sprintf("%s-r", stackName)
  378. namespace := fmt.Sprintf("porter-stack-%s", stackName)
  379. return &helm.InstallChartConfig{
  380. Chart: chart,
  381. Name: releaseName,
  382. Namespace: namespace,
  383. Values: values,
  384. Cluster: cluster,
  385. Repo: repo,
  386. Registries: registries,
  387. }, nil
  388. }