Răsfoiți Sursa

endpoint for creating new apps (#3411)

ianedwards 2 ani în urmă
părinte
comite
6aef6bc390

+ 1 - 1
api/client/porter_app.go

@@ -188,7 +188,7 @@ func (c *Client) ValidatePorterApp(
 		DeploymentTargetId: deploymentTarget,
 	}
 
-	err := c.getRequest(
+	err := c.postRequest(
 		fmt.Sprintf(
 			"/projects/%d/clusters/%d/apps/validate",
 			projectID, clusterID,

+ 238 - 0
api/server/handlers/porter_app/create_app.go

@@ -0,0 +1,238 @@
+package porter_app
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+// CreateAppHandler is the handler for the /apps/create endpoint
+type CreateAppHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewCreateAppHandler handles POST requests to the endpoint /apps/create
+func NewCreateAppHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *CreateAppHandler {
+	return &CreateAppHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+// SourceType is a string type specifying the source type of an app. This is specified in the incoming request
+type SourceType string
+
+const (
+	// SourceType_Github is the source kind for a github repo
+	SourceType_Github SourceType = "github"
+	// SourceType_DockerRegistry is the source kind for an app using an image from a docker registry
+	SourceType_DockerRegistry SourceType = "docker-registry"
+)
+
+// Image is the image used by an app with a docker registry source
+type Image struct {
+	Repository string `json:"repository"`
+	Tag        string `json:"tag"`
+}
+
+// CreateAppRequest is the request object for the /apps/create endpoint
+type CreateAppRequest struct {
+	Name           string     `json:"name"`
+	SourceType     SourceType `json:"source_type"`
+	GitBranch      string     `json:"git_branch"`
+	GitRepoName    string     `json:"git_repo_name"`
+	GitRepoID      uint       `json:"git_repo_id"`
+	PorterYamlPath string     `json:"porter_yaml_path"`
+	Image          *Image     `json:"image,omitempty"`
+}
+
+// CreateGithubAppInput is the input for creating an app with a github source
+type CreateGithubAppInput struct {
+	ProjectID           uint
+	ClusterID           uint
+	Name                string
+	GitBranch           string
+	GitRepoName         string
+	PorterYamlPath      string
+	GitRepoID           uint
+	PorterAppRepository repository.PorterAppRepository
+}
+
+// CreateDockerRegistryAppInput is the input for creating an app with a docker registry source
+type CreateDockerRegistryAppInput struct {
+	ProjectID           uint
+	ClusterID           uint
+	Name                string
+	Repository          string
+	Tag                 string
+	PorterAppRepository repository.PorterAppRepository
+}
+
+func (c *CreateAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-create-app")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	if !project.ValidateApplyV2 {
+		err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled")
+		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		return
+	}
+
+	request := &CreateAppRequest{}
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		err := telemetry.Error(ctx, span, nil, "error decoding request")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	if request.Name == "" {
+		err := telemetry.Error(ctx, span, nil, "name is required")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-name", Value: request.Name})
+
+	if request.SourceType == "" {
+		err := telemetry.Error(ctx, span, nil, "source type is required")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "source-type", Value: request.SourceType})
+
+	var porterApp *types.PorterApp
+	switch request.SourceType {
+	case SourceType_Github:
+		if request.GitRepoID == 0 {
+			err := telemetry.Error(ctx, span, nil, "git repo id is required")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
+
+		if request.GitBranch == "" {
+			err := telemetry.Error(ctx, span, nil, "git branch is required")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
+
+		if request.GitRepoName == "" {
+			err := telemetry.Error(ctx, span, nil, "git repo name is required")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
+
+		telemetry.WithAttributes(span,
+			telemetry.AttributeKV{Key: "git-branch", Value: request.GitBranch},
+			telemetry.AttributeKV{Key: "git-repo-name", Value: request.GitRepoName},
+		)
+
+		input := CreateGithubAppInput{
+			ProjectID:           project.ID,
+			ClusterID:           cluster.ID,
+			Name:                request.Name,
+			GitRepoID:           request.GitRepoID,
+			GitBranch:           request.GitBranch,
+			GitRepoName:         request.GitRepoName,
+			PorterYamlPath:      request.PorterYamlPath,
+			PorterAppRepository: c.Repo().PorterApp(),
+		}
+
+		app, err := createGithubApp(ctx, input)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error creating github app")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+		porterApp = app.ToPorterAppType()
+	case SourceType_DockerRegistry:
+		if request.Image == nil {
+			err := telemetry.Error(ctx, span, nil, "image is required")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
+
+		telemetry.WithAttributes(span,
+			telemetry.AttributeKV{Key: "image-repo-uri", Value: fmt.Sprintf("%s:%s", request.Image.Repository, request.Image.Tag)},
+		)
+
+		input := CreateDockerRegistryAppInput{
+			ProjectID:           project.ID,
+			ClusterID:           cluster.ID,
+			Name:                request.Name,
+			Repository:          request.Image.Repository,
+			Tag:                 request.Image.Tag,
+			PorterAppRepository: c.Repo().PorterApp(),
+		}
+
+		app, err := createDockerRegistryApp(ctx, input)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error creating docker registry app")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+		porterApp = app.ToPorterAppType()
+	default:
+		err := telemetry.Error(ctx, span, nil, "source type not supported")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-id", Value: porterApp.ID})
+
+	c.WriteResult(w, r, porterApp)
+}
+
+func createGithubApp(ctx context.Context, input CreateGithubAppInput) (*models.PorterApp, error) {
+	ctx, span := telemetry.NewSpan(ctx, "create-github-app")
+	defer span.End()
+
+	porterApp := &models.PorterApp{
+		Name:           input.Name,
+		ProjectID:      input.ProjectID,
+		ClusterID:      input.ClusterID,
+		GitRepoID:      input.GitRepoID,
+		GitBranch:      input.GitBranch,
+		RepoName:       input.GitRepoName,
+		PorterYamlPath: input.PorterYamlPath,
+	}
+
+	porterApp, err := input.PorterAppRepository.CreatePorterApp(porterApp)
+	if err != nil {
+		return porterApp, telemetry.Error(ctx, span, err, "error creating porter app")
+	}
+
+	return porterApp, nil
+}
+
+func createDockerRegistryApp(ctx context.Context, input CreateDockerRegistryAppInput) (*models.PorterApp, error) {
+	ctx, span := telemetry.NewSpan(ctx, "create-docker-registry-app")
+	defer span.End()
+
+	porterApp := &models.PorterApp{
+		Name:         input.Name,
+		ProjectID:    input.ProjectID,
+		ClusterID:    input.ClusterID,
+		ImageRepoURI: fmt.Sprintf("%s:%s", input.Repository, input.Tag),
+	}
+
+	porterApp, err := input.PorterAppRepository.CreatePorterApp(porterApp)
+	if err != nil {
+		return porterApp, telemetry.Error(ctx, span, err, "error creating porter app")
+	}
+
+	return porterApp, nil
+}

+ 31 - 2
api/server/router/porter_app.go

@@ -600,11 +600,11 @@ func getPorterAppRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/validate -> porter_app.NewValidatePorterAppHandler
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/apps/validate -> porter_app.NewValidatePorterAppHandler
 	validatePorterAppEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
-			Method: types.HTTPVerbGet,
+			Method: types.HTTPVerbPost,
 			Path: &types.Path{
 				Parent:       basePath,
 				RelativePath: "/apps/validate",
@@ -629,6 +629,35 @@ func getPorterAppRoutes(
 		Router:   r,
 	})
 
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/apps/create -> porter_app.NewCreateAppHandler
+	createAppEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/apps/create",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	createAppHandler := porter_app.NewCreateAppHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: createAppEndpoint,
+		Handler:  createAppHandler,
+		Router:   r,
+	})
+
 	// POST /api/projects/{project_id}/clusters/{cluster_id}/apps/apply -> porter_app.NewApplyPorterAppHandler
 	applyPorterAppEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{