create.go 12 KB

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