create_porter_app.go 11 KB

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