create_porter_app.go 12 KB

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