create_app.go 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. package porter_app
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "github.com/porter-dev/porter/api/server/handlers"
  7. "github.com/porter-dev/porter/api/server/shared"
  8. "github.com/porter-dev/porter/api/server/shared/apierrors"
  9. "github.com/porter-dev/porter/api/server/shared/config"
  10. "github.com/porter-dev/porter/api/types"
  11. "github.com/porter-dev/porter/internal/models"
  12. "github.com/porter-dev/porter/internal/repository"
  13. "github.com/porter-dev/porter/internal/telemetry"
  14. )
  15. // CreateAppHandler is the handler for the /apps/create endpoint
  16. type CreateAppHandler struct {
  17. handlers.PorterHandlerReadWriter
  18. }
  19. // NewCreateAppHandler handles POST requests to the endpoint /apps/create
  20. func NewCreateAppHandler(
  21. config *config.Config,
  22. decoderValidator shared.RequestDecoderValidator,
  23. writer shared.ResultWriter,
  24. ) *CreateAppHandler {
  25. return &CreateAppHandler{
  26. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  27. }
  28. }
  29. // SourceType is a string type specifying the source type of an app. This is specified in the incoming request
  30. type SourceType string
  31. const (
  32. // SourceType_Github is the source kind for a github repo
  33. SourceType_Github SourceType = "github"
  34. // SourceType_DockerRegistry is the source kind for an app using an image from a docker registry
  35. SourceType_DockerRegistry SourceType = "docker-registry"
  36. // SourceType_Local is the source kind for an app being built locally
  37. SourceType_Local SourceType = "other"
  38. )
  39. // Image is the image used by an app with a docker registry source
  40. type Image struct {
  41. Repository string `json:"repository"`
  42. Tag string `json:"tag"`
  43. }
  44. // CreateAppRequest is the request object for the /apps/create endpoint
  45. type CreateAppRequest struct {
  46. Name string `json:"name"`
  47. SourceType SourceType `json:"type"`
  48. GitBranch string `json:"git_branch"`
  49. GitRepoName string `json:"git_repo_name"`
  50. GitRepoID uint `json:"git_repo_id"`
  51. PorterYamlPath string `json:"porter_yaml_path"`
  52. Image *Image `json:"image,omitempty"`
  53. }
  54. // CreateGithubAppInput is the input for creating an app with a github source
  55. type CreateGithubAppInput struct {
  56. ProjectID uint
  57. ClusterID uint
  58. Name string
  59. GitBranch string
  60. GitRepoName string
  61. PorterYamlPath string
  62. GitRepoID uint
  63. PorterAppRepository repository.PorterAppRepository
  64. }
  65. // CreateDockerRegistryAppInput is the input for creating an app with a docker registry source
  66. type CreateDockerRegistryAppInput struct {
  67. ProjectID uint
  68. ClusterID uint
  69. Name string
  70. Repository string
  71. Tag string
  72. PorterAppRepository repository.PorterAppRepository
  73. }
  74. // CreateLocalAppInput is the input for creating an app that is built locally via the cli
  75. type CreateLocalAppInput struct {
  76. ProjectID uint
  77. ClusterID uint
  78. Name string
  79. PorterAppRepository repository.PorterAppRepository
  80. }
  81. func (c *CreateAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  82. ctx, span := telemetry.NewSpan(r.Context(), "serve-create-app")
  83. defer span.End()
  84. project, _ := ctx.Value(types.ProjectScope).(*models.Project)
  85. cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
  86. if !project.ValidateApplyV2 {
  87. err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled")
  88. c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
  89. return
  90. }
  91. request := &CreateAppRequest{}
  92. if ok := c.DecodeAndValidate(w, r, request); !ok {
  93. err := telemetry.Error(ctx, span, nil, "error decoding request")
  94. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  95. return
  96. }
  97. if request.Name == "" {
  98. err := telemetry.Error(ctx, span, nil, "name is required")
  99. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  100. return
  101. }
  102. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-name", Value: request.Name})
  103. if request.SourceType == "" {
  104. err := telemetry.Error(ctx, span, nil, "source type is required")
  105. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  106. return
  107. }
  108. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "source-type", Value: request.SourceType})
  109. porterAppDBEntries, err := c.Repo().PorterApp().ReadPorterAppsByProjectIDAndName(project.ID, request.Name)
  110. if err != nil {
  111. err := telemetry.Error(ctx, span, nil, "error reading porter apps by project id and name")
  112. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  113. return
  114. }
  115. if len(porterAppDBEntries) > 1 {
  116. err := telemetry.Error(ctx, span, nil, "multiple apps with same name")
  117. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  118. return
  119. }
  120. if len(porterAppDBEntries) == 1 {
  121. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "existing-app-id", Value: porterAppDBEntries[0].ID})
  122. c.WriteResult(w, r, porterAppDBEntries[0].ToPorterAppType())
  123. return
  124. }
  125. var porterApp *types.PorterApp
  126. switch request.SourceType {
  127. case SourceType_Github:
  128. if request.GitRepoID == 0 {
  129. err := telemetry.Error(ctx, span, nil, "git repo id is required")
  130. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  131. return
  132. }
  133. if request.GitBranch == "" {
  134. err := telemetry.Error(ctx, span, nil, "git branch is required")
  135. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  136. return
  137. }
  138. if request.GitRepoName == "" {
  139. err := telemetry.Error(ctx, span, nil, "git repo name is required")
  140. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  141. return
  142. }
  143. telemetry.WithAttributes(span,
  144. telemetry.AttributeKV{Key: "git-branch", Value: request.GitBranch},
  145. telemetry.AttributeKV{Key: "git-repo-name", Value: request.GitRepoName},
  146. )
  147. input := CreateGithubAppInput{
  148. ProjectID: project.ID,
  149. ClusterID: cluster.ID,
  150. Name: request.Name,
  151. GitRepoID: request.GitRepoID,
  152. GitBranch: request.GitBranch,
  153. GitRepoName: request.GitRepoName,
  154. PorterYamlPath: request.PorterYamlPath,
  155. PorterAppRepository: c.Repo().PorterApp(),
  156. }
  157. app, err := createGithubApp(ctx, input)
  158. if err != nil {
  159. err := telemetry.Error(ctx, span, err, "error creating github app")
  160. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  161. return
  162. }
  163. porterApp = app.ToPorterAppType()
  164. case SourceType_DockerRegistry:
  165. if request.Image == nil {
  166. err := telemetry.Error(ctx, span, nil, "image is required")
  167. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  168. return
  169. }
  170. telemetry.WithAttributes(span,
  171. telemetry.AttributeKV{Key: "image-repo-uri", Value: fmt.Sprintf("%s:%s", request.Image.Repository, request.Image.Tag)},
  172. )
  173. input := CreateDockerRegistryAppInput{
  174. ProjectID: project.ID,
  175. ClusterID: cluster.ID,
  176. Name: request.Name,
  177. Repository: request.Image.Repository,
  178. Tag: request.Image.Tag,
  179. PorterAppRepository: c.Repo().PorterApp(),
  180. }
  181. app, err := createDockerRegistryApp(ctx, input)
  182. if err != nil {
  183. err := telemetry.Error(ctx, span, err, "error creating docker registry app")
  184. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  185. return
  186. }
  187. porterApp = app.ToPorterAppType()
  188. case SourceType_Local:
  189. input := CreateLocalAppInput{
  190. ProjectID: project.ID,
  191. ClusterID: cluster.ID,
  192. Name: request.Name,
  193. PorterAppRepository: c.Repo().PorterApp(),
  194. }
  195. app, err := createLocalApp(ctx, input)
  196. if err != nil {
  197. err := telemetry.Error(ctx, span, err, "error creating other app")
  198. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  199. return
  200. }
  201. porterApp = app.ToPorterAppType()
  202. default:
  203. err := telemetry.Error(ctx, span, nil, "source type not supported")
  204. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  205. return
  206. }
  207. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-id", Value: porterApp.ID})
  208. c.WriteResult(w, r, porterApp)
  209. }
  210. func createGithubApp(ctx context.Context, input CreateGithubAppInput) (*models.PorterApp, error) {
  211. ctx, span := telemetry.NewSpan(ctx, "create-github-app")
  212. defer span.End()
  213. porterApp := &models.PorterApp{
  214. Name: input.Name,
  215. ProjectID: input.ProjectID,
  216. ClusterID: input.ClusterID,
  217. GitRepoID: input.GitRepoID,
  218. GitBranch: input.GitBranch,
  219. RepoName: input.GitRepoName,
  220. PorterYamlPath: input.PorterYamlPath,
  221. }
  222. porterApp, err := input.PorterAppRepository.CreatePorterApp(porterApp)
  223. if err != nil {
  224. return porterApp, telemetry.Error(ctx, span, err, "error creating porter app")
  225. }
  226. return porterApp, nil
  227. }
  228. func createDockerRegistryApp(ctx context.Context, input CreateDockerRegistryAppInput) (*models.PorterApp, error) {
  229. ctx, span := telemetry.NewSpan(ctx, "create-docker-registry-app")
  230. defer span.End()
  231. porterApp := &models.PorterApp{
  232. Name: input.Name,
  233. ProjectID: input.ProjectID,
  234. ClusterID: input.ClusterID,
  235. ImageRepoURI: fmt.Sprintf("%s:%s", input.Repository, input.Tag),
  236. }
  237. porterApp, err := input.PorterAppRepository.CreatePorterApp(porterApp)
  238. if err != nil {
  239. return porterApp, telemetry.Error(ctx, span, err, "error creating porter app")
  240. }
  241. return porterApp, nil
  242. }
  243. func createLocalApp(ctx context.Context, input CreateLocalAppInput) (*models.PorterApp, error) {
  244. ctx, span := telemetry.NewSpan(ctx, "create-local-app")
  245. defer span.End()
  246. porterApp := &models.PorterApp{
  247. Name: input.Name,
  248. ProjectID: input.ProjectID,
  249. ClusterID: input.ClusterID,
  250. }
  251. porterApp, err := input.PorterAppRepository.CreatePorterApp(porterApp)
  252. if err != nil {
  253. return porterApp, telemetry.Error(ctx, span, err, "error creating porter app")
  254. }
  255. return porterApp, nil
  256. }