create_porter_app.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. package stacks
  2. import (
  3. "encoding/base64"
  4. "fmt"
  5. "net/http"
  6. "strings"
  7. "github.com/porter-dev/porter/api/server/authz"
  8. "github.com/porter-dev/porter/api/server/handlers"
  9. "github.com/porter-dev/porter/api/server/shared"
  10. "github.com/porter-dev/porter/api/server/shared/apierrors"
  11. "github.com/porter-dev/porter/api/server/shared/config"
  12. "github.com/porter-dev/porter/api/server/shared/requestutils"
  13. "github.com/porter-dev/porter/api/types"
  14. "github.com/porter-dev/porter/internal/helm"
  15. "github.com/porter-dev/porter/internal/helm/loader"
  16. "github.com/porter-dev/porter/internal/models"
  17. "github.com/porter-dev/porter/internal/repository"
  18. "github.com/stefanmcshane/helm/pkg/chart"
  19. )
  20. type CreatePorterAppHandler struct {
  21. handlers.PorterHandlerReadWriter
  22. authz.KubernetesAgentGetter
  23. }
  24. func NewCreatePorterAppHandler(
  25. config *config.Config,
  26. decoderValidator shared.RequestDecoderValidator,
  27. writer shared.ResultWriter,
  28. ) *CreatePorterAppHandler {
  29. return &CreatePorterAppHandler{
  30. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  31. KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
  32. }
  33. }
  34. func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  35. ctx := r.Context()
  36. project, _ := ctx.Value(types.ProjectScope).(*models.Project)
  37. cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
  38. request := &types.CreatePorterAppRequest{}
  39. if ok := c.DecodeAndValidate(w, r, request); !ok {
  40. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error decoding request")))
  41. return
  42. }
  43. stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
  44. if reqErr != nil {
  45. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
  46. return
  47. }
  48. namespace := fmt.Sprintf("porter-stack-%s", stackName)
  49. helmAgent, err := c.GetHelmAgent(r, cluster, namespace)
  50. if err != nil {
  51. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error getting helm agent: %w", err)))
  52. return
  53. }
  54. k8sAgent, err := c.GetAgent(r, cluster, namespace)
  55. if err != nil {
  56. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error getting k8s agent: %w", err)))
  57. return
  58. }
  59. helmRelease, err := helmAgent.GetRelease(stackName, 0, false)
  60. shouldCreate := err != nil
  61. porterYamlBase64 := request.PorterYAMLBase64
  62. porterYaml, err := base64.StdEncoding.DecodeString(porterYamlBase64)
  63. if err != nil {
  64. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error decoding porter.yaml: %w", err)))
  65. return
  66. }
  67. imageInfo := request.ImageInfo
  68. registries, err := c.Repo().Registry().ListRegistriesByProjectID(cluster.ProjectID)
  69. if err != nil {
  70. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error listing registries: %w", err)))
  71. return
  72. }
  73. var releaseValues map[string]interface{}
  74. var releaseDependencies []*chart.Dependency
  75. if shouldCreate || request.OverrideRelease {
  76. releaseValues = nil
  77. releaseDependencies = nil
  78. // this is required because when the front-end sends an update request with overrideRelease=true, it is unable to
  79. // get the image info from the release. unless it is explicitly provided in the request, we avoid overwriting it
  80. // by attempting to get the image info from the release
  81. if helmRelease != nil && (imageInfo.Repository == "" || imageInfo.Tag == "") {
  82. imageInfo = attemptToGetImageInfoFromRelease(helmRelease.Config)
  83. }
  84. } else {
  85. releaseValues = helmRelease.Config
  86. releaseDependencies = helmRelease.Chart.Metadata.Dependencies
  87. }
  88. injectLauncher := strings.Contains(request.Builder, "heroku") ||
  89. strings.Contains(request.Builder, "paketo")
  90. chart, values, releaseJobValues, err := parse(
  91. porterYaml,
  92. imageInfo,
  93. c.Config(),
  94. cluster.ProjectID,
  95. releaseValues,
  96. releaseDependencies,
  97. SubdomainCreateOpts{
  98. k8sAgent: k8sAgent,
  99. dnsRepo: c.Repo().DNSRecord(),
  100. powerDnsClient: c.Config().PowerDNSClient,
  101. appRootDomain: c.Config().ServerConf.AppRootDomain,
  102. stackName: stackName,
  103. },
  104. injectLauncher,
  105. )
  106. if err != nil {
  107. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error parsing porter.yaml into chart and values: %w", err)))
  108. return
  109. }
  110. if shouldCreate {
  111. // create the namespace if it does not exist already
  112. _, err = k8sAgent.CreateNamespace(namespace, nil)
  113. if err != nil {
  114. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating namespace: %w", err)))
  115. return
  116. }
  117. // create the release job chart if it does not exist (only done by front-end currently, where we set overrideRelease=true)
  118. if request.OverrideRelease && releaseJobValues != nil {
  119. conf, err := createReleaseJobChart(
  120. stackName,
  121. releaseJobValues,
  122. c.Config().ServerConf.DefaultApplicationHelmRepoURL,
  123. registries,
  124. cluster,
  125. c.Repo(),
  126. )
  127. if err != nil {
  128. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error making config for release job chart: %w", err)))
  129. return
  130. }
  131. _, err = helmAgent.InstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  132. if err != nil {
  133. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating release job chart: %w", err)))
  134. _, err = helmAgent.UninstallChart(fmt.Sprintf("%s-r", stackName))
  135. if err != nil {
  136. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error uninstalling release job chart: %w", err)))
  137. }
  138. return
  139. }
  140. }
  141. conf := &helm.InstallChartConfig{
  142. Chart: chart,
  143. Name: stackName,
  144. Namespace: namespace,
  145. Values: values,
  146. Cluster: cluster,
  147. Repo: c.Repo(),
  148. Registries: registries,
  149. }
  150. // create the app chart
  151. _, err = helmAgent.InstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  152. if err != nil {
  153. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deploying app: %s", err.Error())))
  154. _, err = helmAgent.UninstallChart(stackName)
  155. if err != nil {
  156. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error uninstalling chart: %w", err)))
  157. }
  158. return
  159. }
  160. existing, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
  161. if err != nil {
  162. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  163. return
  164. } else if existing.Name != "" {
  165. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  166. fmt.Errorf("porter app with name %s already exists in this environment", existing.Name), http.StatusForbidden))
  167. return
  168. }
  169. app := &models.PorterApp{
  170. Name: stackName,
  171. ClusterID: cluster.ID,
  172. ProjectID: project.ID,
  173. RepoName: request.RepoName,
  174. GitRepoID: request.GitRepoID,
  175. GitBranch: request.GitBranch,
  176. BuildContext: request.BuildContext,
  177. Builder: request.Builder,
  178. Buildpacks: request.Buildpacks,
  179. Dockerfile: request.Dockerfile,
  180. ImageRepoURI: request.ImageRepoURI,
  181. PullRequestURL: request.PullRequestURL,
  182. }
  183. // create the db entry
  184. porterApp, err := c.Repo().PorterApp().UpdatePorterApp(app)
  185. if err != nil {
  186. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error writing app to DB: %s", err.Error())))
  187. return
  188. }
  189. c.WriteResult(w, r, porterApp.ToPorterAppType())
  190. } else {
  191. // create/update the release job chart
  192. if request.OverrideRelease && releaseJobValues != nil {
  193. releaseJobName := fmt.Sprintf("%s-r", stackName)
  194. helmRelease, err := helmAgent.GetRelease(releaseJobName, 0, false)
  195. if err != nil {
  196. // here the user has created a release job for an already created app, so we need to create and install the release job chart
  197. conf, err := createReleaseJobChart(
  198. stackName,
  199. releaseJobValues,
  200. c.Config().ServerConf.DefaultApplicationHelmRepoURL,
  201. registries,
  202. cluster,
  203. c.Repo(),
  204. )
  205. if err != nil {
  206. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error making config for release job chart: %w", err)))
  207. return
  208. }
  209. _, err = helmAgent.InstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  210. if err != nil {
  211. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating release job chart: %w", err)))
  212. _, err = helmAgent.UninstallChart(fmt.Sprintf("%s-r", stackName))
  213. if err != nil {
  214. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error uninstalling release job chart: %w", err)))
  215. }
  216. return
  217. }
  218. } else {
  219. conf := &helm.UpgradeReleaseConfig{
  220. Name: helmRelease.Name,
  221. Cluster: cluster,
  222. Repo: c.Repo(),
  223. Registries: registries,
  224. Values: releaseJobValues,
  225. }
  226. _, err = helmAgent.UpgradeReleaseByValues(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection, false)
  227. if err != nil {
  228. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error upgrading release job chart: %w", err)))
  229. return
  230. }
  231. }
  232. }
  233. // update the app chart
  234. conf := &helm.InstallChartConfig{
  235. Chart: chart,
  236. Name: stackName,
  237. Namespace: namespace,
  238. Values: values,
  239. Cluster: cluster,
  240. Repo: c.Repo(),
  241. Registries: registries,
  242. }
  243. // update the chart
  244. _, err = helmAgent.UpgradeInstallChart(conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  245. if err != nil {
  246. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deploying app: %s", err.Error())))
  247. return
  248. }
  249. // update the DB entry
  250. app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
  251. if err != nil {
  252. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  253. return
  254. }
  255. if request.RepoName != "" {
  256. app.RepoName = request.RepoName
  257. }
  258. if request.GitBranch != "" {
  259. app.GitBranch = request.GitBranch
  260. }
  261. if request.BuildContext != "" {
  262. app.BuildContext = request.BuildContext
  263. }
  264. if request.Builder != "" {
  265. app.Builder = request.Builder
  266. }
  267. if request.Buildpacks != "" {
  268. if request.Buildpacks == "null" {
  269. app.Buildpacks = ""
  270. } else {
  271. app.Buildpacks = request.Buildpacks
  272. }
  273. }
  274. if request.Dockerfile != "" {
  275. if request.Dockerfile == "null" {
  276. app.Dockerfile = ""
  277. } else {
  278. app.Dockerfile = request.Dockerfile
  279. }
  280. }
  281. if request.ImageRepoURI != "" {
  282. app.ImageRepoURI = request.ImageRepoURI
  283. }
  284. if request.PullRequestURL != "" {
  285. app.PullRequestURL = request.PullRequestURL
  286. }
  287. updatedPorterApp, err := c.Repo().PorterApp().UpdatePorterApp(app)
  288. if err != nil {
  289. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error writing updated app to DB: %s", err.Error())))
  290. return
  291. }
  292. c.WriteResult(w, r, updatedPorterApp.ToPorterAppType())
  293. }
  294. }
  295. func createReleaseJobChart(
  296. stackName string,
  297. values map[string]interface{},
  298. repoUrl string,
  299. registries []*models.Registry,
  300. cluster *models.Cluster,
  301. repo repository.Repository,
  302. ) (*helm.InstallChartConfig, error) {
  303. chart, err := loader.LoadChartPublic(repoUrl, "job", "")
  304. if err != nil {
  305. return nil, err
  306. }
  307. releaseName := fmt.Sprintf("%s-r", stackName)
  308. namespace := fmt.Sprintf("porter-stack-%s", stackName)
  309. return &helm.InstallChartConfig{
  310. Chart: chart,
  311. Name: releaseName,
  312. Namespace: namespace,
  313. Values: values,
  314. Cluster: cluster,
  315. Repo: repo,
  316. Registries: registries,
  317. }, nil
  318. }