create_app.go 12 KB

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