Prechádzať zdrojové kódy

Merge branch 'nafees/pe-features' into dev

Mohammed Nafees 3 rokov pred
rodič
commit
d3c76cc074
33 zmenil súbory, kde vykonal 464 pridanie a 286 odobranie
  1. 54 0
      api/server/authz/preview_environment.go
  2. 0 3
      api/server/handlers/environment/common.go
  3. 0 8
      api/server/handlers/environment/create.go
  4. 0 8
      api/server/handlers/environment/create_deployment.go
  5. 0 8
      api/server/handlers/environment/delete.go
  6. 0 8
      api/server/handlers/environment/delete_deployment.go
  7. 20 8
      api/server/handlers/environment/enable_pull_request.go
  8. 45 11
      api/server/handlers/environment/finalize_deployment.go
  9. 36 14
      api/server/handlers/environment/finalize_deployment_with_errors.go
  10. 40 21
      api/server/handlers/environment/get_deployment.go
  11. 39 15
      api/server/handlers/environment/get_deployment_by_env.go
  12. 0 8
      api/server/handlers/environment/get_environment.go
  13. 0 8
      api/server/handlers/environment/list.go
  14. 0 8
      api/server/handlers/environment/list_deployments.go
  15. 0 8
      api/server/handlers/environment/list_deployments_by_cluster.go
  16. 0 8
      api/server/handlers/environment/reenable_deployment.go
  17. 0 8
      api/server/handlers/environment/toggle_new_comment.go
  18. 0 8
      api/server/handlers/environment/trigger_deployment_workflow.go
  19. 41 16
      api/server/handlers/environment/update_deployment.go
  20. 45 11
      api/server/handlers/environment/update_deployment_status.go
  21. 0 8
      api/server/handlers/environment/validate_porter_yaml.go
  22. 20 0
      api/server/handlers/webhook/github_incoming.go
  23. 10 0
      api/server/router/cluster.go
  24. 9 0
      api/server/router/git_installation.go
  25. 5 0
      api/server/router/router.go
  26. 10 5
      api/types/environment.go
  27. 4 5
      api/types/policy.go
  28. 13 11
      cli/cmd/apply.go
  29. 2 2
      dashboard/package.json
  30. 71 49
      dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventList.tsx
  31. 0 1
      internal/repository/environment.go
  32. 0 14
      internal/repository/gorm/environment.go
  33. 0 4
      internal/repository/test/environment.go

+ 54 - 0
api/server/authz/preview_environment.go

@@ -0,0 +1,54 @@
+package authz
+
+import (
+	"errors"
+	"net/http"
+
+	"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"
+)
+
+var (
+	errPreviewProjectDisabled = errors.New("preview environments are not enabled for this project")
+	errPreviewClusterDisabled = errors.New("preview environments are not enabled for this cluster")
+)
+
+type PreviewEnvironmentScopedFactory struct {
+	config *config.Config
+}
+
+func NewPreviewEnvironmentScopedFactory(
+	config *config.Config,
+) *PreviewEnvironmentScopedFactory {
+	return &PreviewEnvironmentScopedFactory{config}
+}
+
+func (p *PreviewEnvironmentScopedFactory) Middleware(next http.Handler) http.Handler {
+	return &PreviewEnvironmentScopedMiddleware{next, p.config}
+}
+
+type PreviewEnvironmentScopedMiddleware struct {
+	next   http.Handler
+	config *config.Config
+}
+
+func (p *PreviewEnvironmentScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	if !project.PreviewEnvsEnabled {
+		apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r,
+			apierrors.NewErrForbidden(errPreviewProjectDisabled), true)
+		return
+	} else if !cluster.PreviewEnvsEnabled {
+		apierrors.HandleAPIError(p.config.Logger, p.config.Alerter, w, r,
+			apierrors.NewErrForbidden(errPreviewClusterDisabled), true)
+		return
+	}
+
+	// FIXME: use this middleware to also get values for environment_id and deployment_id
+
+	p.next.ServeHTTP(w, r)
+}

+ 0 - 3
api/server/handlers/environment/common.go

@@ -14,9 +14,6 @@ import (
 )
 
 var (
-	errPreviewProjectDisabled = errors.New("preview environments are not enabled for this project")
-	errPreviewClusterDisabled = errors.New("preview environments are not enabled for this cluster")
-
 	errDeploymentNotFound  = errors.New("no such deployment exists")
 	errEnvironmentNotFound = errors.New("no such environment exists")
 	errGithubAPI           = errors.New("error communicating with the github API")

+ 0 - 8
api/server/handlers/environment/create.go

@@ -41,14 +41,6 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {

+ 0 - 8
api/server/handlers/environment/create_deployment.go

@@ -40,14 +40,6 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {

+ 0 - 8
api/server/handlers/environment/delete.go

@@ -41,14 +41,6 @@ func (c *DeleteEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {

+ 0 - 8
api/server/handlers/environment/delete_deployment.go

@@ -38,14 +38,6 @@ func (c *DeleteDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	deplID, reqErr := requestutils.GetURLParamUint(r, "deployment_id")
 
 	if reqErr != nil {

+ 20 - 8
api/server/handlers/environment/enable_pull_request.go

@@ -37,14 +37,6 @@ func (c *EnablePullRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	request := &types.PullRequest{}
 
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
@@ -122,4 +114,24 @@ func (c *EnablePullRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
+
+	// create the deployment
+	depl, err := c.Repo().Environment().CreateDeployment(&models.Deployment{
+		EnvironmentID: env.ID,
+		Namespace:     "namespace-creating",
+		Status:        types.DeploymentStatusCreating,
+		PullRequestID: request.Number,
+		RepoOwner:     request.RepoOwner,
+		RepoName:      request.RepoName,
+		PRName:        request.Title,
+		PRBranchFrom:  request.BranchFrom,
+		PRBranchInto:  request.BranchInto,
+	})
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, depl.ToDeploymentType())
 }

+ 45 - 11
api/server/handlers/environment/finalize_deployment.go

@@ -2,6 +2,7 @@ package environment
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net/http"
 	"strings"
@@ -16,6 +17,7 @@ import (
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
 )
 
 type FinalizeDeploymentHandler struct {
@@ -37,14 +39,6 @@ func (c *FinalizeDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
@@ -57,19 +51,59 @@ func (c *FinalizeDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 		return
 	}
 
+	if request.Namespace == "" && request.PRNumber == 0 {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("either namespace or pr_number must be present in request body"), http.StatusBadRequest,
+		))
+		return
+	}
+
+	var err error
+
 	// read the environment to get the environment id
 	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID), owner, name)
 
 	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(errEnvironmentNotFound))
+			return
+		}
+
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
+	var depl *models.Deployment
+
 	// read the deployment
-	depl, err := c.Repo().Environment().ReadDeployment(env.ID, request.Namespace)
+	if request.PRNumber != 0 {
+		depl, err = c.Repo().Environment().ReadDeploymentByGitDetails(env.ID, owner, name, request.PRNumber)
 
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		if err != nil {
+			if errors.Is(err, gorm.ErrRecordNotFound) {
+				c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
+				return
+			}
+
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	} else if request.Namespace != "" {
+		depl, err = c.Repo().Environment().ReadDeployment(env.ID, request.Namespace)
+
+		if err != nil {
+			if errors.Is(err, gorm.ErrRecordNotFound) {
+				c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
+				return
+			}
+
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	}
+
+	if depl == nil {
+		c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
 		return
 	}
 

+ 36 - 14
api/server/handlers/environment/finalize_deployment_with_errors.go

@@ -37,14 +37,6 @@ func (c *FinalizeDeploymentWithErrorsHandler) ServeHTTP(w http.ResponseWriter, r
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
@@ -57,6 +49,13 @@ func (c *FinalizeDeploymentWithErrorsHandler) ServeHTTP(w http.ResponseWriter, r
 		return
 	}
 
+	if request.Namespace == "" && request.PRNumber == 0 {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("either namespace or pr_number must be present in request body"), http.StatusBadRequest,
+		))
+		return
+	}
+
 	if len(request.Errors) == 0 {
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
 			fmt.Errorf("at least one error is required to report"), http.StatusPreconditionFailed,
@@ -64,12 +63,14 @@ func (c *FinalizeDeploymentWithErrorsHandler) ServeHTTP(w http.ResponseWriter, r
 		return
 	}
 
+	var err error
+
 	// read the environment to get the environment id
 	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID), owner, name)
 
 	if err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("environment not found in cluster and project")))
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(errEnvironmentNotFound))
 			return
 		}
 
@@ -77,16 +78,37 @@ func (c *FinalizeDeploymentWithErrorsHandler) ServeHTTP(w http.ResponseWriter, r
 		return
 	}
 
+	var depl *models.Deployment
+
 	// read the deployment
-	depl, err := c.Repo().Environment().ReadDeployment(env.ID, request.Namespace)
+	if request.PRNumber != 0 {
+		depl, err = c.Repo().Environment().ReadDeploymentByGitDetails(env.ID, owner, name, request.PRNumber)
 
-	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("no deployment found for environment ID: %d, namespace: %s", env.ID, request.Namespace)))
+		if err != nil {
+			if errors.Is(err, gorm.ErrRecordNotFound) {
+				c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
+				return
+			}
+
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 			return
 		}
+	} else if request.Namespace != "" {
+		depl, err = c.Repo().Environment().ReadDeployment(env.ID, request.Namespace)
 
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		if err != nil {
+			if errors.Is(err, gorm.ErrRecordNotFound) {
+				c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
+				return
+			}
+
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	}
+
+	if depl == nil {
+		c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
 		return
 	}
 

+ 40 - 21
api/server/handlers/environment/get_deployment.go

@@ -35,14 +35,6 @@ func (c *GetDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
@@ -55,30 +47,57 @@ func (c *GetDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
+	if request.Namespace == "" && request.PRNumber == 0 {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("either namespace or pr_number must be present in request body"), http.StatusBadRequest,
+		))
+		return
+	}
+
+	var err error
+
 	// read the environment to get the environment id
 	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID), owner, name)
 
 	if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("environment not found: is the environment enabled for this git installation?"),
-			http.StatusNotFound,
-		))
+		c.HandleAPIError(w, r, apierrors.NewErrNotFound(errEnvironmentNotFound))
 		return
 	} else if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	depl, err := c.Repo().Environment().ReadDeployment(env.ID, request.Namespace)
+	var depl *models.Deployment
 
-	if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("deployment not found"),
-			http.StatusNotFound,
-		))
-		return
-	} else if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+	// read the deployment
+	if request.PRNumber != 0 {
+		depl, err = c.Repo().Environment().ReadDeploymentByGitDetails(env.ID, owner, name, request.PRNumber)
+
+		if err != nil {
+			if errors.Is(err, gorm.ErrRecordNotFound) {
+				c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
+				return
+			}
+
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	} else if request.Namespace != "" {
+		depl, err = c.Repo().Environment().ReadDeployment(env.ID, request.Namespace)
+
+		if err != nil {
+			if errors.Is(err, gorm.ErrRecordNotFound) {
+				c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
+				return
+			}
+
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	}
+
+	if depl == nil {
+		c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
 		return
 	}
 

+ 39 - 15
api/server/handlers/environment/get_deployment_by_env.go

@@ -33,14 +33,6 @@ func (c *GetDeploymentByEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	envID, reqErr := requestutils.GetURLParamUint(r, "environment_id")
 
 	if reqErr != nil {
@@ -54,11 +46,20 @@ func (c *GetDeploymentByEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *
 		return
 	}
 
-	_, err := c.Repo().Environment().ReadEnvironmentByID(project.ID, cluster.ID, envID)
+	if request.Namespace == "" && request.PRNumber == 0 {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("either namespace or pr_number must be present in request body"), http.StatusBadRequest,
+		))
+		return
+	}
+
+	var err error
+
+	env, err := c.Repo().Environment().ReadEnvironmentByID(project.ID, cluster.ID, envID)
 
 	if err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("environment with id %d not found", envID)))
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(errEnvironmentNotFound))
 			return
 		}
 
@@ -66,15 +67,38 @@ func (c *GetDeploymentByEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *
 		return
 	}
 
-	depl, err := c.Repo().Environment().ReadDeployment(envID, request.Namespace)
+	var depl *models.Deployment
 
-	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("deployment not found for namespace: %s", request.Namespace)))
+	// read the deployment
+	if request.PRNumber != 0 {
+		depl, err = c.Repo().Environment().ReadDeploymentByGitDetails(env.ID, env.GitRepoOwner, env.GitRepoName,
+			request.PRNumber)
+
+		if err != nil {
+			if errors.Is(err, gorm.ErrRecordNotFound) {
+				c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
+				return
+			}
+
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 			return
 		}
+	} else if request.Namespace != "" {
+		depl, err = c.Repo().Environment().ReadDeployment(env.ID, request.Namespace)
 
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		if err != nil {
+			if errors.Is(err, gorm.ErrRecordNotFound) {
+				c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
+				return
+			}
+
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	}
+
+	if depl == nil {
+		c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
 		return
 	}
 

+ 0 - 8
api/server/handlers/environment/get_environment.go

@@ -32,14 +32,6 @@ func (c *GetEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	envID, reqErr := requestutils.GetURLParamUint(r, "environment_id")
 
 	if reqErr != nil {

+ 0 - 8
api/server/handlers/environment/list.go

@@ -29,14 +29,6 @@ func (c *ListEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	envs, err := c.Repo().Environment().ListEnvironments(project.ID, cluster.ID)
 
 	if err != nil {

+ 0 - 8
api/server/handlers/environment/list_deployments.go

@@ -35,14 +35,6 @@ func (c *ListDeploymentsHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	req := &types.ListDeploymentRequest{}
 
 	if ok := c.DecodeAndValidate(w, r, req); !ok {

+ 0 - 8
api/server/handlers/environment/list_deployments_by_cluster.go

@@ -34,14 +34,6 @@ func (c *ListDeploymentsByClusterHandler) ServeHTTP(w http.ResponseWriter, r *ht
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	req := &types.ListDeploymentRequest{}
 
 	if ok := c.DecodeAndValidate(w, r, req); !ok {

+ 0 - 8
api/server/handlers/environment/reenable_deployment.go

@@ -36,14 +36,6 @@ func (c *ReenableDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	deplID, reqErr := requestutils.GetURLParamUint(r, "deployment_id")
 
 	if reqErr != nil {

+ 0 - 8
api/server/handlers/environment/toggle_new_comment.go

@@ -36,14 +36,6 @@ func (c *ToggleNewCommentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	environmentID, reqErr := requestutils.GetURLParamUint(r, "environment_id")
 
 	if reqErr != nil {

+ 0 - 8
api/server/handlers/environment/trigger_deployment_workflow.go

@@ -37,14 +37,6 @@ func (c *TriggerDeploymentWorkflowHandler) ServeHTTP(w http.ResponseWriter, r *h
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	deplID, reqErr := requestutils.GetURLParamUint(r, "deployment_id")
 
 	if reqErr != nil {

+ 41 - 16
api/server/handlers/environment/update_deployment.go

@@ -1,6 +1,7 @@
 package environment
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 
@@ -13,6 +14,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models/integrations"
+	"gorm.io/gorm"
 )
 
 type UpdateDeploymentHandler struct {
@@ -36,14 +38,6 @@ func (c *UpdateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
@@ -56,6 +50,15 @@ func (c *UpdateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		return
 	}
 
+	if request.Namespace == "" && request.PRNumber == 0 {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("either namespace or pr_number must be present in request body"), http.StatusBadRequest,
+		))
+		return
+	}
+
+	var err error
+
 	// read the environment to get the environment id
 	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID), owner, name)
 
@@ -64,11 +67,37 @@ func (c *UpdateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		return
 	}
 
+	var depl *models.Deployment
+
 	// read the deployment
-	depl, err := c.Repo().Environment().ReadDeployment(env.ID, request.Namespace)
+	if request.PRNumber != 0 {
+		depl, err = c.Repo().Environment().ReadDeploymentByGitDetails(env.ID, owner, name, request.PRNumber)
+
+		if err != nil {
+			if errors.Is(err, gorm.ErrRecordNotFound) {
+				c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
+				return
+			}
+
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	} else if request.Namespace != "" {
+		depl, err = c.Repo().Environment().ReadDeployment(env.ID, request.Namespace)
+
+		if err != nil {
+			if errors.Is(err, gorm.ErrRecordNotFound) {
+				c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
+				return
+			}
+
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	}
 
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+	if depl == nil {
+		c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
 		return
 	}
 
@@ -104,14 +133,10 @@ func (c *UpdateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		return
 	}
 
+	depl.Namespace = request.Namespace
 	depl.GHDeploymentID = ghDeployment.GetID()
 	depl.CommitSHA = request.CommitSHA
 
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
 	// update the deployment
 	depl, err = c.Repo().Environment().UpdateDeployment(depl)
 

+ 45 - 11
api/server/handlers/environment/update_deployment_status.go

@@ -1,6 +1,7 @@
 package environment
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 
@@ -13,6 +14,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models/integrations"
+	"gorm.io/gorm"
 )
 
 type UpdateDeploymentStatusHandler struct {
@@ -36,14 +38,6 @@ func (c *UpdateDeploymentStatusHandler) ServeHTTP(w http.ResponseWriter, r *http
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
 
 	if !ok {
@@ -56,19 +50,59 @@ func (c *UpdateDeploymentStatusHandler) ServeHTTP(w http.ResponseWriter, r *http
 		return
 	}
 
+	if request.Namespace == "" && request.PRNumber == 0 {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("either namespace or pr_number must be present in request body"), http.StatusBadRequest,
+		))
+		return
+	}
+
+	var err error
+
 	// read the environment to get the environment id
 	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID), owner, name)
 
 	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(errEnvironmentNotFound))
+			return
+		}
+
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
+	var depl *models.Deployment
+
 	// read the deployment
-	depl, err := c.Repo().Environment().ReadDeployment(env.ID, request.Namespace)
+	if request.PRNumber != 0 {
+		depl, err = c.Repo().Environment().ReadDeploymentByGitDetails(env.ID, owner, name, request.PRNumber)
+
+		if err != nil {
+			if errors.Is(err, gorm.ErrRecordNotFound) {
+				c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
+				return
+			}
+
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	} else if request.Namespace != "" {
+		depl, err = c.Repo().Environment().ReadDeployment(env.ID, request.Namespace)
+
+		if err != nil {
+			if errors.Is(err, gorm.ErrRecordNotFound) {
+				c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
+				return
+			}
+
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	}
 
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+	if depl == nil {
+		c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
 		return
 	}
 

+ 0 - 8
api/server/handlers/environment/validate_porter_yaml.go

@@ -40,14 +40,6 @@ func (c *ValidatePorterYAMLHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
-	if !project.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
-		return
-	} else if !cluster.PreviewEnvsEnabled {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
-		return
-	}
-
 	envID, reqErr := requestutils.GetURLParamUint(r, "environment_id")
 
 	if reqErr != nil {

+ 20 - 0
api/server/handlers/webhook/github_incoming.go

@@ -114,6 +114,26 @@ func (c *GithubIncomingWebhookHandler) processPullRequestEvent(event *github.Pul
 	}
 
 	if env.Mode == "auto" && event.GetAction() == "opened" {
+		depl := &models.Deployment{
+			EnvironmentID: env.ID,
+			Namespace:     "namespace-creating",
+			Status:        types.DeploymentStatusCreating,
+			PullRequestID: uint(event.GetPullRequest().GetNumber()),
+			PRName:        event.GetPullRequest().GetTitle(),
+			RepoName:      repo,
+			RepoOwner:     owner,
+			CommitSHA:     event.GetPullRequest().GetHead().GetSHA()[:7],
+			PRBranchFrom:  event.GetPullRequest().GetHead().GetRef(),
+			PRBranchInto:  event.GetPullRequest().GetBase().GetRef(),
+		}
+
+		_, err = c.Repo().Environment().CreateDeployment(depl)
+
+		if err != nil {
+			return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d, prNumber: %d] "+
+				"error creating new deployment: %w", webhookID, owner, repo, env.ID, event.GetPullRequest().GetNumber(), err)
+		}
+
 		_, err := client.Actions.CreateWorkflowDispatchEventByFileName(
 			r.Context(), owner, repo, fmt.Sprintf("porter_%s_env.yml", env.Name),
 			github.CreateWorkflowDispatchEventRequest{

+ 10 - 0
api/server/router/cluster.go

@@ -303,6 +303,7 @@ func getClusterRoutes(
 					types.UserScope,
 					types.ProjectScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)
@@ -331,6 +332,7 @@ func getClusterRoutes(
 					types.UserScope,
 					types.ProjectScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)
@@ -359,6 +361,7 @@ func getClusterRoutes(
 					types.UserScope,
 					types.ProjectScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)
@@ -388,6 +391,7 @@ func getClusterRoutes(
 					types.UserScope,
 					types.ProjectScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)
@@ -417,6 +421,7 @@ func getClusterRoutes(
 					types.UserScope,
 					types.ProjectScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)
@@ -446,6 +451,7 @@ func getClusterRoutes(
 					types.UserScope,
 					types.ProjectScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)
@@ -475,6 +481,7 @@ func getClusterRoutes(
 					types.UserScope,
 					types.ProjectScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)
@@ -504,6 +511,7 @@ func getClusterRoutes(
 					types.UserScope,
 					types.ProjectScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)
@@ -533,6 +541,7 @@ func getClusterRoutes(
 					types.UserScope,
 					types.ProjectScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)
@@ -563,6 +572,7 @@ func getClusterRoutes(
 					types.UserScope,
 					types.ProjectScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)

+ 9 - 0
api/server/router/git_installation.go

@@ -135,6 +135,7 @@ func getGitInstallationRoutes(
 					types.ProjectScope,
 					types.GitInstallationScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)
@@ -171,6 +172,7 @@ func getGitInstallationRoutes(
 					types.ProjectScope,
 					types.GitInstallationScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)
@@ -207,6 +209,7 @@ func getGitInstallationRoutes(
 					types.ProjectScope,
 					types.GitInstallationScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)
@@ -243,6 +246,7 @@ func getGitInstallationRoutes(
 					types.ProjectScope,
 					types.GitInstallationScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)
@@ -279,6 +283,7 @@ func getGitInstallationRoutes(
 					types.ProjectScope,
 					types.GitInstallationScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)
@@ -315,6 +320,7 @@ func getGitInstallationRoutes(
 					types.ProjectScope,
 					types.GitInstallationScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)
@@ -351,6 +357,7 @@ func getGitInstallationRoutes(
 					types.ProjectScope,
 					types.GitInstallationScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)
@@ -387,6 +394,7 @@ func getGitInstallationRoutes(
 					types.ProjectScope,
 					types.GitInstallationScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)
@@ -423,6 +431,7 @@ func getGitInstallationRoutes(
 					types.ProjectScope,
 					types.GitInstallationScope,
 					types.ClusterScope,
+					types.PreviewEnvironmentScope,
 				},
 			},
 		)

+ 5 - 0
api/server/router/router.go

@@ -234,6 +234,9 @@ func registerRoutes(config *config.Config, routes []*router.Route) {
 	// gitlab integration middleware to handle gitlab integrations for a specific project
 	gitlabIntFactory := authz.NewGitlabIntegrationScopedFactory(config)
 
+	// preview environment middleware to handle previw environments for a specific project-cluster pair
+	previewEnvFactory := authz.NewPreviewEnvironmentScopedFactory(config)
+
 	for _, route := range routes {
 		atomicGroup := route.Router.Group(nil)
 
@@ -273,6 +276,8 @@ func registerRoutes(config *config.Config, routes []*router.Route) {
 				atomicGroup.Use(stackFactory.Middleware)
 			case types.GitlabIntegrationScope:
 				atomicGroup.Use(gitlabIntFactory.Middleware)
+			case types.PreviewEnvironmentScope:
+				atomicGroup.Use(previewEnvFactory.Middleware)
 			}
 		}
 

+ 10 - 5
api/types/environment.go

@@ -81,15 +81,17 @@ type SuccessfullyDeployedResource struct {
 }
 
 type FinalizeDeploymentRequest struct {
-	Namespace           string                          `json:"namespace" form:"required"`
+	Namespace           string                          `json:"namespace"`
 	SuccessfulResources []*SuccessfullyDeployedResource `json:"successful_resources"`
 	Subdomain           string                          `json:"subdomain"`
+	PRNumber            uint                            `json:"pr_number"`
 }
 
 type FinalizeDeploymentWithErrorsRequest struct {
-	Namespace           string                          `json:"namespace" form:"required"`
+	Namespace           string                          `json:"namespace"`
 	SuccessfulResources []*SuccessfullyDeployedResource `json:"successful_resources"`
 	Errors              map[string]string               `json:"errors" form:"required"`
+	PRNumber            uint                            `json:"pr_number"`
 }
 
 type UpdateDeploymentRequest struct {
@@ -97,7 +99,8 @@ type UpdateDeploymentRequest struct {
 
 	PRBranchFrom string `json:"gh_pr_branch_from" form:"required"`
 	CommitSHA    string `json:"commit_sha" form:"required"`
-	Namespace    string `json:"namespace" form:"required"`
+	Namespace    string `json:"namespace"`
+	PRNumber     uint   `json:"pr_number"`
 }
 
 type ListDeploymentRequest struct {
@@ -109,7 +112,8 @@ type UpdateDeploymentStatusRequest struct {
 
 	PRBranchFrom string `json:"gh_pr_branch_from" form:"required"`
 	Status       string `json:"status" form:"required,oneof=created creating inactive failed"`
-	Namespace    string `json:"namespace" form:"required"`
+	Namespace    string `json:"namespace"`
+	PRNumber     uint   `json:"pr_number"`
 }
 
 type DeleteDeploymentRequest struct {
@@ -117,7 +121,8 @@ type DeleteDeploymentRequest struct {
 }
 
 type GetDeploymentRequest struct {
-	Namespace string `schema:"namespace" form:"required"`
+	Namespace string `schema:"namespace"`
+	PRNumber  uint   `schema:"pr_number"`
 }
 
 type PullRequest struct {

+ 4 - 5
api/types/policy.go

@@ -20,7 +20,6 @@ const (
 	StackScope              PermissionScope = "stack"
 	GitlabIntegrationScope  PermissionScope = "gitlab_integration"
 	PreviewEnvironmentScope PermissionScope = "preview_environment"
-	EnvironmentScope        PermissionScope = "environment"
 )
 
 type NameOrUInt struct {
@@ -37,7 +36,9 @@ type PolicyDocument struct {
 
 type ScopeTree map[PermissionScope]ScopeTree
 
-/* ScopeHeirarchy describes the tree of scopes, i.e. Cluster, Registry, and Settings
+/*
+	ScopeHeirarchy describes the tree of scopes, i.e. Cluster, Registry, and Settings
+
 are children of Project, Namespace is a child of Cluster, etc.
 */
 var ScopeHeirarchy = ScopeTree{
@@ -47,6 +48,7 @@ var ScopeHeirarchy = ScopeTree{
 				StackScope:   {},
 				ReleaseScope: {},
 			},
+			PreviewEnvironmentScope: {},
 		},
 		RegistryScope:        {},
 		HelmRepoScope:        {},
@@ -57,9 +59,6 @@ var ScopeHeirarchy = ScopeTree{
 		SettingsScope: {
 			InviteScope: {},
 		},
-		PreviewEnvironmentScope: {
-			EnvironmentScope: {},
-		},
 		GitlabIntegrationScope: {},
 	},
 }

+ 13 - 11
cli/cmd/apply.go

@@ -831,7 +831,7 @@ func (t *DeploymentHook) PreApply() error {
 		context.Background(),
 		t.projectID, t.clusterID, t.envID,
 		&types.GetDeploymentRequest{
-			Namespace: t.namespace,
+			PRNumber: t.prID,
 		},
 	)
 
@@ -864,6 +864,7 @@ func (t *DeploymentHook) PreApply() error {
 			t.repoOwner, t.repoName,
 			&types.UpdateDeploymentRequest{
 				Namespace: t.namespace,
+				PRNumber:  t.prID,
 				CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
 					ActionID: t.actionID,
 				},
@@ -952,7 +953,7 @@ func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
 	}
 
 	req := &types.FinalizeDeploymentRequest{
-		Namespace: t.namespace,
+		PRNumber:  t.prID,
 		Subdomain: strings.Join(subdomains, ", "),
 	}
 
@@ -978,23 +979,24 @@ func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
 	return err
 }
 
-func (t *DeploymentHook) OnError(err error) {
+func (t *DeploymentHook) OnError(error) {
 	// if the deployment exists, throw an error for that deployment
-	_, getDeplErr := t.client.GetDeployment(
+	_, err := t.client.GetDeployment(
 		context.Background(),
 		t.projectID, t.clusterID, t.envID,
 		&types.GetDeploymentRequest{
-			Namespace: t.namespace,
+			PRNumber: t.prID,
 		},
 	)
 
-	if getDeplErr == nil {
-		_, err = t.client.UpdateDeploymentStatus(
+	if err == nil {
+		// FIXME: try to use the error with a custom logger
+		t.client.UpdateDeploymentStatus(
 			context.Background(),
 			t.projectID, t.gitInstallationID, t.clusterID,
 			t.repoOwner, t.repoName,
 			&types.UpdateDeploymentStatusRequest{
-				Namespace: t.namespace,
+				PRNumber: t.prID,
 				CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
 					ActionID: t.actionID,
 				},
@@ -1011,14 +1013,14 @@ func (t *DeploymentHook) OnConsolidatedErrors(allErrors map[string]error) {
 		context.Background(),
 		t.projectID, t.clusterID, t.envID,
 		&types.GetDeploymentRequest{
-			Namespace: t.namespace,
+			PRNumber: t.prID,
 		},
 	)
 
 	if getDeplErr == nil {
 		req := &types.FinalizeDeploymentWithErrorsRequest{
-			Namespace: t.namespace,
-			Errors:    make(map[string]string),
+			PRNumber: t.prID,
+			Errors:   make(map[string]string),
 		}
 
 		for _, res := range t.resourceGroup.Resources {

+ 2 - 2
dashboard/package.json

@@ -66,9 +66,9 @@
   },
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",
-    "start": "npx webpack-dev-server",
+    "start": "./node_modules/webpack-dev-server/bin/webpack-dev-server.js",
     "build": "NODE_ENV=\"production\" webpack",
-    "build-and-analyze": "ENABLE_ANALYZER=true NODE_ENV=\"production\" webpack"
+    "build-and-analyze": "ENABLE_ANALYZER=true NODE_ENV=\"production\" ./node_modules/webpack/bin/webpack.js"
   },
   "devDependencies": {
     "@babel/core": "^7.15.0",

+ 71 - 49
dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventList.tsx

@@ -26,6 +26,69 @@ type Props = {
   setLogData?: (logData: InitLogData) => void;
 };
 
+interface ExpandedIncidentLogsProps {
+  logs: Log[];
+  onViewMore: () => void;
+}
+
+const ExpandedIncidentLogs = ({
+  logs,
+  onViewMore,
+}: ExpandedIncidentLogsProps) => {
+  if (!logs.length) {
+    return (
+      <LogsLoadWrapper>
+        <Loading />
+      </LogsLoadWrapper>
+    );
+  }
+
+  return (
+    <LogsSectionWrapper>
+      <StyledLogsSection>
+        {logs?.map((log, i) => {
+          return (
+            <LogSpan key={[log.lineNumber, i].join(".")}>
+              <span className="line-number">{log.lineNumber}.</span>
+              <span className="line-timestamp">
+                {dayjs(log.timestamp).format("MMM D, YYYY HH:mm:ss")}
+              </span>
+              <LogOuter key={[log.lineNumber, i].join(".")}>
+                {log.line?.map((ansi, j) => {
+                  if (ansi.clearLine) {
+                    return null;
+                  }
+
+                  return (
+                    <LogInnerSpan
+                      key={[log.lineNumber, i, j].join(".")}
+                      ansi={ansi}
+                    >
+                      {ansi.content.replace(/ /g, "\u00a0")}
+                    </LogInnerSpan>
+                  );
+                })}
+              </LogOuter>
+            </LogSpan>
+          );
+        })}
+      </StyledLogsSection>
+      <ViewLogsWrapper>
+        <DocsLink
+          onClick={(e) => {
+            e.preventDefault();
+            e.stopPropagation();
+            onViewMore();
+          }}
+        >
+          View complete log history
+          <i className="material-icons">open_in_new</i>{" "}
+        </DocsLink>
+      </ViewLogsWrapper>
+    </LogsSectionWrapper>
+  );
+};
+
 const EventList: React.FC<Props> = ({ filters, namespace, setLogData }) => {
   const { currentProject, currentCluster } = useContext(Context);
   const [events, setEvents] = useState([]);
@@ -108,6 +171,7 @@ const EventList: React.FC<Props> = ({ filters, namespace, setLogData }) => {
       )
       .then((res) => {
         if (!expandedEvent.should_view_logs) {
+          setExpandedIncidentEvents(res.data.events);
           return null;
         }
 
@@ -158,54 +222,12 @@ const EventList: React.FC<Props> = ({ filters, namespace, setLogData }) => {
           <img src={document} />
           {expandedIncidentEvents[0].detail}
         </Message>
-        {logs.length ? (
-          <LogsSectionWrapper>
-            <StyledLogsSection>
-              {logs?.map((log, i) => {
-                return (
-                  <LogSpan key={[log.lineNumber, i].join(".")}>
-                    <span className="line-number">{log.lineNumber}.</span>
-                    <span className="line-timestamp">
-                      {dayjs(log.timestamp).format("MMM D, YYYY HH:mm:ss")}
-                    </span>
-                    <LogOuter key={[log.lineNumber, i].join(".")}>
-                      {log.line?.map((ansi, j) => {
-                        if (ansi.clearLine) {
-                          return null;
-                        }
-
-                        return (
-                          <LogInnerSpan
-                            key={[log.lineNumber, i, j].join(".")}
-                            ansi={ansi}
-                          >
-                            {ansi.content.replace(/ /g, "\u00a0")}
-                          </LogInnerSpan>
-                        );
-                      })}
-                    </LogOuter>
-                  </LogSpan>
-                );
-              })}
-            </StyledLogsSection>
-            <ViewLogsWrapper>
-              <DocsLink
-                onClick={(e) => {
-                  e.preventDefault();
-                  e.stopPropagation();
-                  redirectToLogs(expandedEvent);
-                }}
-              >
-                View complete log history
-                <i className="material-icons">open_in_new</i>{" "}
-              </DocsLink>
-            </ViewLogsWrapper>
-          </LogsSectionWrapper>
-        ) : (
-          <LogsLoadWrapper>
-            <Loading />
-          </LogsLoadWrapper>
-        )}
+        {expandedEvent.should_view_logs ? (
+          <ExpandedIncidentLogs
+            logs={logs}
+            onViewMore={() => redirectToLogs(expandedEvent)}
+          />
+        ) : null}
       </>
     );
   };
@@ -672,4 +694,4 @@ const LogInnerSpan = styled.span`
 export const ViewLogsWrapper = styled.div`
   margin-bottom: -15px;
   margin-top: 15px;
-`;
+`;

+ 0 - 1
internal/repository/environment.go

@@ -14,7 +14,6 @@ type EnvironmentRepository interface {
 	CreateDeployment(deployment *models.Deployment) (*models.Deployment, error)
 	ReadDeployment(environmentID uint, namespace string) (*models.Deployment, error)
 	ReadDeploymentByID(projectID, clusterID, id uint) (*models.Deployment, error)
-	ReadDeploymentByCluster(projectID, clusterID uint, namespace string) (*models.Deployment, error)
 	ReadDeploymentByGitDetails(environmentID uint, owner, repo string, prNumber uint) (*models.Deployment, error)
 	ListDeploymentsByCluster(projectID, clusterID uint, states ...string) ([]*models.Deployment, error)
 	ListDeployments(environmentID uint, states ...string) ([]*models.Deployment, error)

+ 0 - 14
internal/repository/gorm/environment.go

@@ -170,20 +170,6 @@ func (repo *EnvironmentRepository) ReadDeploymentByID(projectID, clusterID, id u
 	return depl, nil
 }
 
-func (repo *EnvironmentRepository) ReadDeploymentByCluster(projectID, clusterID uint, namespace string) (*models.Deployment, error) {
-	depl := &models.Deployment{}
-
-	if err := repo.db.
-		Order("deployments.id asc").
-		Joins("INNER JOIN environments ON environments.id = deployments.environment_id").
-		Where("environments.project_id = ? AND environments.cluster_id = ? AND environments.deleted_at IS NULL AND namespace = ?", projectID, clusterID, depl.Namespace).
-		Find(&depl).Error; err != nil {
-		return nil, err
-	}
-
-	return depl, nil
-}
-
 func (repo *EnvironmentRepository) ReadDeploymentByGitDetails(
 	environmentID uint, gitRepoOwner, gitRepoName string, prNumber uint,
 ) (*models.Deployment, error) {

+ 0 - 4
internal/repository/test/environment.go

@@ -66,10 +66,6 @@ func (repo *EnvironmentRepository) ReadDeploymentByID(projectID, clusterID, id u
 	panic("unimplemented")
 }
 
-func (repo *EnvironmentRepository) ReadDeploymentByCluster(projectID, clusterID uint, namespace string) (*models.Deployment, error) {
-	panic("unimplemented")
-}
-
 func (repo *EnvironmentRepository) ReadDeploymentByGitDetails(environmentID uint, owner, repoName string, prNumber uint) (*models.Deployment, error) {
 	panic("unimplemented")
 }