create_app.go 10 KB

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