create_app.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  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. )
  37. // Image is the image used by an app with a docker registry source
  38. type Image struct {
  39. Repository string `json:"repository"`
  40. Tag string `json:"tag"`
  41. }
  42. // CreateAppRequest is the request object for the /apps/create endpoint
  43. type CreateAppRequest struct {
  44. Name string `json:"name"`
  45. SourceType SourceType `json:"type"`
  46. GitBranch string `json:"git_branch"`
  47. GitRepoName string `json:"git_repo_name"`
  48. GitRepoID uint `json:"git_repo_id"`
  49. PorterYamlPath string `json:"porter_yaml_path"`
  50. Image *Image `json:"image,omitempty"`
  51. }
  52. // CreateGithubAppInput is the input for creating an app with a github source
  53. type CreateGithubAppInput struct {
  54. ProjectID uint
  55. ClusterID uint
  56. Name string
  57. GitBranch string
  58. GitRepoName string
  59. PorterYamlPath string
  60. GitRepoID uint
  61. PorterAppRepository repository.PorterAppRepository
  62. }
  63. // CreateDockerRegistryAppInput is the input for creating an app with a docker registry source
  64. type CreateDockerRegistryAppInput struct {
  65. ProjectID uint
  66. ClusterID uint
  67. Name string
  68. Repository string
  69. Tag string
  70. PorterAppRepository repository.PorterAppRepository
  71. }
  72. func (c *CreateAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  73. ctx, span := telemetry.NewSpan(r.Context(), "serve-create-app")
  74. defer span.End()
  75. project, _ := ctx.Value(types.ProjectScope).(*models.Project)
  76. cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
  77. if !project.ValidateApplyV2 {
  78. err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled")
  79. c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
  80. return
  81. }
  82. request := &CreateAppRequest{}
  83. if ok := c.DecodeAndValidate(w, r, request); !ok {
  84. err := telemetry.Error(ctx, span, nil, "error decoding request")
  85. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  86. return
  87. }
  88. if request.Name == "" {
  89. err := telemetry.Error(ctx, span, nil, "name is required")
  90. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  91. return
  92. }
  93. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-name", Value: request.Name})
  94. if request.SourceType == "" {
  95. err := telemetry.Error(ctx, span, nil, "source type is required")
  96. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  97. return
  98. }
  99. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "source-type", Value: request.SourceType})
  100. var porterApp *types.PorterApp
  101. switch request.SourceType {
  102. case SourceType_Github:
  103. if request.GitRepoID == 0 {
  104. err := telemetry.Error(ctx, span, nil, "git repo id is required")
  105. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  106. return
  107. }
  108. if request.GitBranch == "" {
  109. err := telemetry.Error(ctx, span, nil, "git branch is required")
  110. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  111. return
  112. }
  113. if request.GitRepoName == "" {
  114. err := telemetry.Error(ctx, span, nil, "git repo name is required")
  115. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  116. return
  117. }
  118. telemetry.WithAttributes(span,
  119. telemetry.AttributeKV{Key: "git-branch", Value: request.GitBranch},
  120. telemetry.AttributeKV{Key: "git-repo-name", Value: request.GitRepoName},
  121. )
  122. input := CreateGithubAppInput{
  123. ProjectID: project.ID,
  124. ClusterID: cluster.ID,
  125. Name: request.Name,
  126. GitRepoID: request.GitRepoID,
  127. GitBranch: request.GitBranch,
  128. GitRepoName: request.GitRepoName,
  129. PorterYamlPath: request.PorterYamlPath,
  130. PorterAppRepository: c.Repo().PorterApp(),
  131. }
  132. app, err := createGithubApp(ctx, input)
  133. if err != nil {
  134. err := telemetry.Error(ctx, span, err, "error creating github app")
  135. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  136. return
  137. }
  138. porterApp = app.ToPorterAppType()
  139. case SourceType_DockerRegistry:
  140. if request.Image == nil {
  141. err := telemetry.Error(ctx, span, nil, "image is required")
  142. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  143. return
  144. }
  145. telemetry.WithAttributes(span,
  146. telemetry.AttributeKV{Key: "image-repo-uri", Value: fmt.Sprintf("%s:%s", request.Image.Repository, request.Image.Tag)},
  147. )
  148. input := CreateDockerRegistryAppInput{
  149. ProjectID: project.ID,
  150. ClusterID: cluster.ID,
  151. Name: request.Name,
  152. Repository: request.Image.Repository,
  153. Tag: request.Image.Tag,
  154. PorterAppRepository: c.Repo().PorterApp(),
  155. }
  156. app, err := createDockerRegistryApp(ctx, input)
  157. if err != nil {
  158. err := telemetry.Error(ctx, span, err, "error creating docker registry app")
  159. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  160. return
  161. }
  162. porterApp = app.ToPorterAppType()
  163. default:
  164. err := telemetry.Error(ctx, span, nil, "source type not supported")
  165. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  166. return
  167. }
  168. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-id", Value: porterApp.ID})
  169. c.WriteResult(w, r, porterApp)
  170. }
  171. func createGithubApp(ctx context.Context, input CreateGithubAppInput) (*models.PorterApp, error) {
  172. ctx, span := telemetry.NewSpan(ctx, "create-github-app")
  173. defer span.End()
  174. porterApp := &models.PorterApp{
  175. Name: input.Name,
  176. ProjectID: input.ProjectID,
  177. ClusterID: input.ClusterID,
  178. GitRepoID: input.GitRepoID,
  179. GitBranch: input.GitBranch,
  180. RepoName: input.GitRepoName,
  181. PorterYamlPath: input.PorterYamlPath,
  182. }
  183. porterApp, err := input.PorterAppRepository.CreatePorterApp(porterApp)
  184. if err != nil {
  185. return porterApp, telemetry.Error(ctx, span, err, "error creating porter app")
  186. }
  187. return porterApp, nil
  188. }
  189. func createDockerRegistryApp(ctx context.Context, input CreateDockerRegistryAppInput) (*models.PorterApp, error) {
  190. ctx, span := telemetry.NewSpan(ctx, "create-docker-registry-app")
  191. defer span.End()
  192. porterApp := &models.PorterApp{
  193. Name: input.Name,
  194. ProjectID: input.ProjectID,
  195. ClusterID: input.ClusterID,
  196. ImageRepoURI: fmt.Sprintf("%s:%s", input.Repository, input.Tag),
  197. }
  198. porterApp, err := input.PorterAppRepository.CreatePorterApp(porterApp)
  199. if err != nil {
  200. return porterApp, telemetry.Error(ctx, span, err, "error creating porter app")
  201. }
  202. return porterApp, nil
  203. }