Explorar o código

identify environment and deployments by git repo owner/name as well

Alexander Belanger %!s(int64=4) %!d(string=hai) anos
pai
achega
bc4e19f1f1

+ 15 - 10
api/client/environment.go

@@ -10,14 +10,15 @@ import (
 func (c *Client) CreateDeployment(
 	ctx context.Context,
 	projID, gitInstallationID, clusterID uint,
+	gitRepoOwner, gitRepoName string,
 	req *types.CreateDeploymentRequest,
 ) (*types.Deployment, error) {
 	resp := &types.Deployment{}
 
 	err := c.postRequest(
 		fmt.Sprintf(
-			"/projects/%d/gitrepos/%d/clusters/%d/deployment",
-			projID, gitInstallationID, clusterID,
+			"/projects/%d/gitrepos/%d/%s/%s/clusters/%d/deployment",
+			projID, gitInstallationID, gitRepoOwner, gitRepoName, clusterID,
 		),
 		req,
 		resp,
@@ -29,14 +30,15 @@ func (c *Client) CreateDeployment(
 func (c *Client) GetDeployment(
 	ctx context.Context,
 	projID, gitInstallationID, clusterID uint,
+	gitRepoOwner, gitRepoName string,
 	req *types.GetDeploymentRequest,
 ) (*types.Deployment, error) {
 	resp := &types.Deployment{}
 
 	err := c.getRequest(
 		fmt.Sprintf(
-			"/projects/%d/gitrepos/%d/clusters/%d/deployment",
-			projID, gitInstallationID, clusterID,
+			"/projects/%d/gitrepos/%d/%s/%s/clusters/%d/deployment",
+			projID, gitInstallationID, gitRepoOwner, gitRepoName, clusterID,
 		),
 		req,
 		resp,
@@ -48,14 +50,15 @@ func (c *Client) GetDeployment(
 func (c *Client) UpdateDeployment(
 	ctx context.Context,
 	projID, gitInstallationID, clusterID uint,
+	gitRepoOwner, gitRepoName string,
 	req *types.UpdateDeploymentRequest,
 ) (*types.Deployment, error) {
 	resp := &types.Deployment{}
 
 	err := c.postRequest(
 		fmt.Sprintf(
-			"/projects/%d/gitrepos/%d/clusters/%d/deployment/update",
-			projID, gitInstallationID, clusterID,
+			"/projects/%d/gitrepos/%d/%s/%s/clusters/%d/deployment/update",
+			projID, gitInstallationID, gitRepoOwner, gitRepoName, clusterID,
 		),
 		req,
 		resp,
@@ -67,14 +70,15 @@ func (c *Client) UpdateDeployment(
 func (c *Client) FinalizeDeployment(
 	ctx context.Context,
 	projID, gitInstallationID, clusterID uint,
+	gitRepoOwner, gitRepoName string,
 	req *types.FinalizeDeploymentRequest,
 ) (*types.Deployment, error) {
 	resp := &types.Deployment{}
 
 	err := c.postRequest(
 		fmt.Sprintf(
-			"/projects/%d/gitrepos/%d/clusters/%d/deployment/finalize",
-			projID, gitInstallationID, clusterID,
+			"/projects/%d/gitrepos/%d/%s/%s/clusters/%d/deployment/finalize",
+			projID, gitInstallationID, gitRepoOwner, gitRepoName, clusterID,
 		),
 		req,
 		resp,
@@ -86,12 +90,13 @@ func (c *Client) FinalizeDeployment(
 func (c *Client) DeleteDeployment(
 	ctx context.Context,
 	projID, gitInstallationID, clusterID uint,
+	gitRepoOwner, gitRepoName string,
 	req *types.DeleteDeploymentRequest,
 ) error {
 	return c.deleteRequest(
 		fmt.Sprintf(
-			"/projects/%d/gitrepos/%d/clusters/%d/deployment",
-			projID, gitInstallationID, clusterID,
+			"/projects/%d/gitrepos/%d/%s/%s/clusters/%d/deployment",
+			projID, gitInstallationID, gitRepoOwner, gitRepoName, clusterID,
 		),
 		req,
 		nil,

+ 11 - 4
api/server/handlers/environment/create.go

@@ -7,6 +7,7 @@ import (
 	ghinstallation "github.com/bradleyfalzon/ghinstallation/v2"
 	"github.com/google/go-github/v41/github"
 	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"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"
@@ -37,6 +38,12 @@ 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)
 
+	owner, name, ok := gitinstallation.GetOwnerAndNameParams(c, w, r)
+
+	if !ok {
+		return
+	}
+
 	// create the environment
 	request := &types.CreateEnvironmentRequest{}
 
@@ -49,8 +56,8 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		ClusterID:         cluster.ID,
 		GitInstallationID: uint(ga.InstallationID),
 		Name:              request.Name,
-		GitRepoOwner:      request.GitRepoOwner,
-		GitRepoName:       request.GitRepoName,
+		GitRepoOwner:      owner,
+		GitRepoName:       name,
 	})
 
 	if err != nil {
@@ -85,8 +92,8 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		Client:            client,
 		ServerURL:         c.Config().ServerConf.ServerURL,
 		PorterToken:       encoded,
-		GitRepoOwner:      request.GitRepoOwner,
-		GitRepoName:       request.GitRepoName,
+		GitRepoOwner:      owner,
+		GitRepoName:       name,
 		ProjectID:         project.ID,
 		ClusterID:         cluster.ID,
 		GitInstallationID: uint(ga.InstallationID),

+ 16 - 9
api/server/handlers/environment/create_deployment.go

@@ -8,6 +8,7 @@ import (
 	"github.com/google/go-github/v41/github"
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"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"
@@ -37,6 +38,12 @@ 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)
 
+	owner, name, ok := gitinstallation.GetOwnerAndNameParams(c, w, r)
+
+	if !ok {
+		return
+	}
+
 	request := &types.CreateDeploymentRequest{}
 
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
@@ -44,7 +51,7 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	}
 
 	// read the environment to get the environment id
-	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID))
+	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID), owner, name)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
@@ -68,15 +75,15 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 
 	// create the deployment
 	depl, err := c.Repo().Environment().CreateDeployment(&models.Deployment{
-		EnvironmentID:      env.ID,
-		Namespace:          request.Namespace,
-		Status:             "creating",
-		PullRequestID:      request.PullRequestID,
+		EnvironmentID:  env.ID,
+		Namespace:      request.Namespace,
+		Status:         "creating",
+		PullRequestID:  request.PullRequestID,
 		GHDeploymentID: ghDeployment.GetID(),
-		RepoOwner: request.GitHubMetadata.RepoOwner,
-		RepoName: request.GitHubMetadata.RepoName,
-		PRName: request.GitHubMetadata.PRName,
-		CommitSHA: request.GitHubMetadata.CommitSHA,
+		RepoOwner:      request.GitHubMetadata.RepoOwner,
+		RepoName:       request.GitHubMetadata.RepoName,
+		PRName:         request.GitHubMetadata.PRName,
+		CommitSHA:      request.GitHubMetadata.CommitSHA,
 	})
 
 	if err != nil {

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

@@ -4,6 +4,7 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"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"
@@ -32,8 +33,14 @@ 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)
 
+	owner, name, ok := gitinstallation.GetOwnerAndNameParams(c, w, r)
+
+	if !ok {
+		return
+	}
+
 	// read the environment to get the environment id
-	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID))
+	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID), owner, name)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

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

@@ -8,6 +8,7 @@ import (
 	"github.com/google/go-github/v41/github"
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"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"
@@ -37,6 +38,12 @@ 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)
 
+	owner, name, ok := gitinstallation.GetOwnerAndNameParams(c, w, r)
+
+	if !ok {
+		return
+	}
+
 	request := &types.DeleteDeploymentRequest{}
 
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
@@ -44,7 +51,7 @@ func (c *DeleteDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	}
 
 	// read the environment to get the environment id
-	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID))
+	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID), owner, name)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 8 - 1
api/server/handlers/environment/finalize_deployment.go

@@ -7,6 +7,7 @@ import (
 
 	"github.com/google/go-github/v41/github"
 	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"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"
@@ -34,6 +35,12 @@ 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)
 
+	owner, name, ok := gitinstallation.GetOwnerAndNameParams(c, w, r)
+
+	if !ok {
+		return
+	}
+
 	request := &types.FinalizeDeploymentRequest{}
 
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
@@ -41,7 +48,7 @@ func (c *FinalizeDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 	}
 
 	// read the environment to get the environment id
-	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID))
+	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID), owner, name)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 8 - 1
api/server/handlers/environment/get_deployment.go

@@ -6,6 +6,7 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"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"
@@ -34,6 +35,12 @@ 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)
 
+	owner, name, ok := gitinstallation.GetOwnerAndNameParams(c, w, r)
+
+	if !ok {
+		return
+	}
+
 	request := &types.GetDeploymentRequest{}
 
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
@@ -41,7 +48,7 @@ func (c *GetDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	}
 
 	// read the environment to get the environment id
-	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID))
+	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(

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

@@ -6,6 +6,7 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"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"
@@ -34,8 +35,14 @@ 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)
 
+	owner, name, ok := gitinstallation.GetOwnerAndNameParams(c, w, r)
+
+	if !ok {
+		return
+	}
+
 	// read the environment to get the environment id
-	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID))
+	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(

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

@@ -0,0 +1,46 @@
+package environment
+
+import (
+	"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"
+)
+
+type ListDeploymentsByClusterHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewListDeploymentsByClusterHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ListDeploymentsByClusterHandler {
+	return &ListDeploymentsByClusterHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *ListDeploymentsByClusterHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	depls, err := c.Repo().Environment().ListDeploymentsByCluster(project.ID, cluster.ID)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := make([]*types.Deployment, 0)
+
+	for _, depl := range depls {
+		res = append(res, depl.ToDeploymentType())
+	}
+
+	c.WriteResult(w, r, res)
+}

+ 8 - 1
api/server/handlers/environment/update_deployment.go

@@ -5,6 +5,7 @@ import (
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"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"
@@ -34,6 +35,12 @@ 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)
 
+	owner, name, ok := gitinstallation.GetOwnerAndNameParams(c, w, r)
+
+	if !ok {
+		return
+	}
+
 	request := &types.UpdateDeploymentRequest{}
 
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
@@ -41,7 +48,7 @@ func (c *UpdateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	}
 
 	// read the environment to get the environment id
-	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID))
+	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID), owner, name)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

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

@@ -287,6 +287,35 @@ func getClusterRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/deployments -> environment.NewListDeploymentsByClusterHandler
+	listDeploymentsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/deployments",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	listDeploymentsHandler := environment.NewListDeploymentsByClusterHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: listDeploymentsEndpoint,
+		Handler:  listDeploymentsHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/namespaces -> cluster.NewClusterListNamespacesHandler
 	listNamespacesEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 64 - 24
api/server/router/git_installation.go

@@ -83,15 +83,20 @@ func getGitInstallationRoutes(
 		Router:   r,
 	})
 
-	// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/clusters/{cluster_id} ->
+	// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id} ->
 	// environment.NewCreateEnvironmentHandler
 	createEnvironmentEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbCreate,
 			Method: types.HTTPVerbPost,
 			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath + "/clusters/{cluster_id}/environment",
+				Parent: basePath,
+				RelativePath: fmt.Sprintf(
+					"%s/{%s}/{%s}/clusters/{cluster_id}/environment",
+					relPath,
+					types.URLParamGitRepoName,
+					types.URLParamGitBranch,
+				),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -114,15 +119,20 @@ func getGitInstallationRoutes(
 		Router:   r,
 	})
 
-	// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/clusters/{cluster_id}/deployment ->
+	// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployment ->
 	// environment.NewCreateDeploymentHandler
 	createDeploymentEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbCreate,
 			Method: types.HTTPVerbPost,
 			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath + "/clusters/{cluster_id}/deployment",
+				Parent: basePath,
+				RelativePath: fmt.Sprintf(
+					"%s/{%s}/{%s}/clusters/{cluster_id}/deployment",
+					relPath,
+					types.URLParamGitRepoName,
+					types.URLParamGitBranch,
+				),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -145,15 +155,20 @@ func getGitInstallationRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/gitrepos/{git_installation_id}/clusters/{cluster_id}/deployment ->
+	// GET /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployment ->
 	// environment.NewCreateDeploymentHandler
 	getDeploymentEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath + "/clusters/{cluster_id}/deployment",
+				Parent: basePath,
+				RelativePath: fmt.Sprintf(
+					"%s/{%s}/{%s}/clusters/{cluster_id}/deployment",
+					relPath,
+					types.URLParamGitRepoName,
+					types.URLParamGitBranch,
+				),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -176,15 +191,20 @@ func getGitInstallationRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/gitrepos/{git_installation_id}/clusters/{cluster_id}/deployments ->
+	// GET /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployments ->
 	// environment.NewCreateDeploymentHandler
 	listDeploymentsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath + "/clusters/{cluster_id}/deployments",
+				Parent: basePath,
+				RelativePath: fmt.Sprintf(
+					"%s/{%s}/{%s}/clusters/{cluster_id}/deployments",
+					relPath,
+					types.URLParamGitRepoName,
+					types.URLParamGitBranch,
+				),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -207,15 +227,20 @@ func getGitInstallationRoutes(
 		Router:   r,
 	})
 
-	// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/clusters/{cluster_id}/deployment/finalize ->
+	// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployment/finalize ->
 	// environment.NewFinalizeDeploymentHandler
 	finalizeDeploymentEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbCreate,
 			Method: types.HTTPVerbPost,
 			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath + "/clusters/{cluster_id}/deployment/finalize",
+				Parent: basePath,
+				RelativePath: fmt.Sprintf(
+					"%s/{%s}/{%s}/clusters/{cluster_id}/deployment/finalize",
+					relPath,
+					types.URLParamGitRepoName,
+					types.URLParamGitBranch,
+				),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -238,15 +263,20 @@ func getGitInstallationRoutes(
 		Router:   r,
 	})
 
-	// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/clusters/{cluster_id}/deployment/update ->
+	// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployment/update ->
 	// environment.NewFinalizeDeploymentHandler
 	updateDeploymentEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbUpdate,
 			Method: types.HTTPVerbPost,
 			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath + "/clusters/{cluster_id}/deployment/update",
+				Parent: basePath,
+				RelativePath: fmt.Sprintf(
+					"%s/{%s}/{%s}/clusters/{cluster_id}/deployment/update",
+					relPath,
+					types.URLParamGitRepoName,
+					types.URLParamGitBranch,
+				),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -269,15 +299,20 @@ func getGitInstallationRoutes(
 		Router:   r,
 	})
 
-	// DELETE /api/projects/{project_id}/gitrepos/{git_installation_id}/clusters/{cluster_id}/environment ->
+	// DELETE /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/environment ->
 	// environment.NewDeleteEnvironmentHandler
 	deleteEnvironmentEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbDelete,
 			Method: types.HTTPVerbDelete,
 			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath + "/clusters/{cluster_id}/environment",
+				Parent: basePath,
+				RelativePath: fmt.Sprintf(
+					"%s/{%s}/{%s}/clusters/{cluster_id}/environment",
+					relPath,
+					types.URLParamGitRepoName,
+					types.URLParamGitBranch,
+				),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -300,15 +335,20 @@ func getGitInstallationRoutes(
 		Router:   r,
 	})
 
-	// DELETE /api/projects/{project_id}/gitrepos/{git_installation_id}/clusters/{cluster_id}/deployment ->
+	// DELETE /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/deployment ->
 	// environment.NewDeleteDeploymentHandler
 	deleteDeploymentEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbDelete,
 			Method: types.HTTPVerbDelete,
 			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath + "/clusters/{cluster_id}/deployment",
+				Parent: basePath,
+				RelativePath: fmt.Sprintf(
+					"%s/{%s}/{%s}/clusters/{cluster_id}/deployment",
+					relPath,
+					types.URLParamGitRepoName,
+					types.URLParamGitBranch,
+				),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,

+ 14 - 16
api/types/environment.go

@@ -12,28 +12,26 @@ type Environment struct {
 }
 
 type CreateEnvironmentRequest struct {
-	Name         string `json:"name" form:"required"`
-	GitRepoOwner string `json:"git_repo_owner" form:"required"`
-	GitRepoName  string `json:"git_repo_name" form:"required"`
+	Name string `json:"name" form:"required"`
 }
 
 type GitHubMetadata struct {
-	DeploymentID int64		`json:"gh_deployment_id"`
-	PRName		 string		`json:"gh_pr_name"`
-	RepoName	 string		`json:"gh_repo_name"`
-	RepoOwner	 string		`json:"gh_repo_owner"`
-	CommitSHA	 string		`json:"gh_commit_sha"`
+	DeploymentID int64  `json:"gh_deployment_id"`
+	PRName       string `json:"gh_pr_name"`
+	RepoName     string `json:"gh_repo_name"`
+	RepoOwner    string `json:"gh_repo_owner"`
+	CommitSHA    string `json:"gh_commit_sha"`
 }
 
 type Deployment struct {
 	*GitHubMetadata
-	
-	ID                 uint   `json:"id"`
-	EnvironmentID      uint   `json:"environment_id"`
-	Namespace          string `json:"namespace"`
-	Status             string `json:"status"`
-	Subdomain          string `json:"subdomain"`
-	PullRequestID      uint   `json:"pull_request_id"`
+
+	ID            uint   `json:"id"`
+	EnvironmentID uint   `json:"environment_id"`
+	Namespace     string `json:"namespace"`
+	Status        string `json:"status"`
+	Subdomain     string `json:"subdomain"`
+	PullRequestID uint   `json:"pull_request_id"`
 }
 
 type CreateGHDeploymentRequest struct {
@@ -57,7 +55,7 @@ type FinalizeDeploymentRequest struct {
 type UpdateDeploymentRequest struct {
 	*CreateGHDeploymentRequest
 
-	CommitSHA string `json:"commit_sha" form:"required"`	
+	CommitSHA string `json:"commit_sha" form:"required"`
 	Namespace string `json:"namespace" form:"required"`
 }
 

+ 10 - 6
cli/cmd/apply.go

@@ -610,10 +610,10 @@ func existsInRepo(name, version, url string) (map[string]interface{}, error) {
 }
 
 type DeploymentHook struct {
-	client                                                  *api.Client
-	resourceGroup                                           *switchboardTypes.ResourceGroup
-	gitInstallationID, projectID, clusterID, prID, actionID uint
-	branch, namespace, repoName, repoOwner, prName, commitSHA             string
+	client                                                    *api.Client
+	resourceGroup                                             *switchboardTypes.ResourceGroup
+	gitInstallationID, projectID, clusterID, prID, actionID   uint
+	branch, namespace, repoName, repoOwner, prName, commitSHA string
 }
 
 func NewDeploymentHook(client *api.Client, resourceGroup *switchboardTypes.ResourceGroup, namespace string) (*DeploymentHook, error) {
@@ -711,6 +711,7 @@ func (t *DeploymentHook) PreApply() error {
 	_, err := t.client.GetDeployment(
 		context.Background(),
 		t.projectID, t.gitInstallationID, t.clusterID,
+		t.repoOwner, t.repoName,
 		&types.GetDeploymentRequest{
 			Namespace: t.namespace,
 		},
@@ -722,6 +723,7 @@ func (t *DeploymentHook) PreApply() error {
 		_, err = t.client.CreateDeployment(
 			context.Background(),
 			t.projectID, t.gitInstallationID, t.clusterID,
+			t.repoOwner, t.repoName,
 			&types.CreateDeploymentRequest{
 				Namespace:     t.namespace,
 				PullRequestID: t.prID,
@@ -730,8 +732,8 @@ func (t *DeploymentHook) PreApply() error {
 					ActionID: t.actionID,
 				},
 				GitHubMetadata: &types.GitHubMetadata{
-					PRName:		t.prName,
-					RepoName: t.repoName,
+					PRName:    t.prName,
+					RepoName:  t.repoName,
 					RepoOwner: t.repoOwner,
 					CommitSHA: t.commitSHA,
 				},
@@ -741,6 +743,7 @@ func (t *DeploymentHook) PreApply() error {
 		_, err = t.client.UpdateDeployment(
 			context.Background(),
 			t.projectID, t.gitInstallationID, t.clusterID,
+			t.repoOwner, t.repoName,
 			&types.UpdateDeploymentRequest{
 				Namespace: t.namespace,
 				CreateGHDeploymentRequest: &types.CreateGHDeploymentRequest{
@@ -801,6 +804,7 @@ func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
 	_, err := t.client.FinalizeDeployment(
 		context.Background(),
 		t.projectID, t.gitInstallationID, t.clusterID,
+		t.repoOwner, t.repoName,
 		&types.FinalizeDeploymentRequest{
 			Namespace: t.namespace,
 			Subdomain: strings.Join(subdomains, ","),

+ 16 - 0
cli/cmd/delete.go

@@ -79,9 +79,25 @@ func delete(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []st
 		return fmt.Errorf("Git installation ID must be defined, set by PORTER_GIT_INSTALLATION_ID")
 	}
 
+	var gitRepoName string
+	var gitRepoOwner string
+
+	if repoName := os.Getenv("PORTER_REPO_NAME"); repoName != "" {
+		gitRepoName = repoName
+	} else if repoName == "" {
+		return fmt.Errorf("Repo name must be defined, set by PORTER_REPO_NAME")
+	}
+
+	if repoOwner := os.Getenv("PORTER_REPO_OWNER"); repoOwner != "" {
+		gitRepoOwner = repoOwner
+	} else if repoOwner == "" {
+		return fmt.Errorf("Repo owner must be defined, set by PORTER_REPO_OWNER")
+	}
+
 	return client.DeleteDeployment(
 		context.Background(),
 		projectID, ghID, clusterID,
+		gitRepoOwner, gitRepoName,
 		&types.DeleteDeploymentRequest{
 			Namespace: deplNamespace,
 		},

+ 44 - 39
dashboard/src/main/home/cluster-dashboard/dashboard/preview-environments/EnvironmentDetail.tsx

@@ -27,27 +27,30 @@ const EnvironmentDetail = () => {
 
   const useQuery = () => {
     const { search } = useLocation();
-  
+
     return React.useMemo(() => new URLSearchParams(search), [search]);
-  }
+  };
 
   useEffect(() => {
     let query = useQuery();
     let isSubscribed = true;
 
-    let git_installation_id = parseInt(query.get("git_installation_id"))
+    let git_installation_id = parseInt(query.get("git_installation_id"));
     api
-    .getPRDeployment(
-      "<token>",
-      {
-        namespace: params.namespace
-      },
-      {
-        project_id: currentProject.id,
-        cluster_id: currentCluster.id,
-        git_installation_id: git_installation_id
-      }
-    ).then(({ data }) => {
+      .getPRDeployment(
+        "<token>",
+        {
+          namespace: params.namespace,
+        },
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+          git_installation_id: git_installation_id,
+          git_repo_owner: environment.gh_repo_owner,
+          git_repo_name: environment.gh_repo_name,
+        }
+      )
+      .then(({ data }) => {
         if (!isSubscribed) {
           return;
         }
@@ -67,16 +70,16 @@ const EnvironmentDetail = () => {
         }
       });
 
-      return () => {
-        isSubscribed = false;
-      };  
+    return () => {
+      isSubscribed = false;
+    };
   }, [params]);
 
   if (!environment) {
     return <Loading />;
   }
 
-  let repository = `${environment.gh_repo_owner}/${environment.gh_repo_name}`
+  let repository = `${environment.gh_repo_owner}/${environment.gh_repo_name}`;
 
   return (
     <StyledExpandedChart>
@@ -87,31 +90,30 @@ const EnvironmentDetail = () => {
         <Title icon={pr_icon} iconWidth="25px">
           {environment.gh_pr_name}
           <DeploymentImageContainer>
-              <DeploymentTypeIcon src={integrationList.repo.icon} />
-              <RepositoryName
-                onMouseOver={() => {
-                  setShowRepoTooltip(true);
-                }}
-                onMouseOut={() => {
-                  setShowRepoTooltip(false);
-                }}
-              >
-                {repository}
-              </RepositoryName>
-              {showRepoTooltip && <Tooltip>{repository}</Tooltip>}
-            </DeploymentImageContainer>
+            <DeploymentTypeIcon src={integrationList.repo.icon} />
+            <RepositoryName
+              onMouseOver={() => {
+                setShowRepoTooltip(true);
+              }}
+              onMouseOut={() => {
+                setShowRepoTooltip(false);
+              }}
+            >
+              {repository}
+            </RepositoryName>
+            {showRepoTooltip && <Tooltip>{repository}</Tooltip>}
+          </DeploymentImageContainer>
           <TagWrapper>
             Namespace <NamespaceTag>{environment.namespace}</NamespaceTag>
           </TagWrapper>
         </Title>
         <InfoWrapper>
-          {
-            environment.subdomain && <PRLink to={environment.subdomain} target="_blank">
-            <i className="material-icons">link</i>
-            {environment.subdomain}
-          </PRLink>
-
-          }
+          {environment.subdomain && (
+            <PRLink to={environment.subdomain} target="_blank">
+              <i className="material-icons">link</i>
+              {environment.subdomain}
+            </PRLink>
+          )}
         </InfoWrapper>
         <Flex>
           <Status>
@@ -119,7 +121,10 @@ const EnvironmentDetail = () => {
             {capitalize(environment.status)}
           </Status>
           <Dot>•</Dot>
-          <GHALink to={`https://github.com/${repository}/pull/${environment.pull_request_id}`} target="_blank">
+          <GHALink
+            to={`https://github.com/${repository}/pull/${environment.pull_request_id}`}
+            target="_blank"
+          >
             <img src={github} /> GitHub
             <i className="material-icons">open_in_new</i>
           </GHALink>

+ 103 - 127
dashboard/src/main/home/cluster-dashboard/dashboard/preview-environments/EnvironmentList.tsx

@@ -13,28 +13,28 @@ import pr_icon from "assets/pull_request_icon.svg";
 import _ from "lodash";
 
 export type PRDeployment = {
-  id: number,
-  subdomain: string,
-  status: string,
-  environment_id: number,
-  pull_request_id: number,
-  namespace: string,
-  git_installation_id?: number,
-  gh_pr_name: string,
-  gh_repo_owner: string,
-  gh_repo_name: string,
-  gh_commit_sha: string,
-}
+  id: number;
+  subdomain: string;
+  status: string;
+  environment_id: number;
+  pull_request_id: number;
+  namespace: string;
+  git_installation_id?: number;
+  gh_pr_name: string;
+  gh_repo_owner: string;
+  gh_repo_name: string;
+  gh_commit_sha: string;
+};
 
 export type Environment = {
-  id: Number,
-  project_id: number,
-  cluster_id: number,
-  git_installation_id: number,
-  name: string,
-  git_repo_owner: string,
-  git_repo_name: string,
-}
+  id: Number;
+  project_id: number;
+  cluster_id: number;
+  git_installation_id: number;
+  name: string;
+  git_repo_owner: string;
+  git_repo_name: string;
+};
 
 export const capitalize = (s: string) => {
   return s.charAt(0).toUpperCase() + s.substring(1).toLowerCase();
@@ -51,15 +51,14 @@ const EnvironmentList = () => {
     Context
   );
 
-
   const { url: currentUrl } = useRouteMatch();
 
   const location = useLocation();
   const history = useHistory();
 
-    useEffect(() => {
-      let isSubscribed = true;
-      api
+  useEffect(() => {
+    let isSubscribed = true;
+    api
       .listEnvironments(
         "<token>",
         {},
@@ -67,42 +66,41 @@ const EnvironmentList = () => {
           project_id: currentProject.id,
           cluster_id: currentCluster.id,
         }
-      ).then(({ data }) => {
-          if (!isSubscribed) {
-            return;
-          }
-  
-          if (!Array.isArray(data)) {
-            throw Error("Data is not an array");
-          }
-          setEnvironmentList(data);
-        })
-        .catch((err) => {
-          console.error(err);
-          if (isSubscribed) {
-            setHasError(true);
-          }
-        })
-  
-        return () => {
-          isSubscribed = false;
-        };  
-    }, []);
-
-  
-  const getDeployments = (git_installation_id: number) => {
+      )
+      .then(({ data }) => {
+        if (!isSubscribed) {
+          return;
+        }
+
+        if (!Array.isArray(data)) {
+          throw Error("Data is not an array");
+        }
+        setEnvironmentList(data);
+      })
+      .catch((err) => {
+        console.error(err);
+        if (isSubscribed) {
+          setHasError(true);
+        }
+      });
+
+    return () => {
+      isSubscribed = false;
+    };
+  }, []);
+
+  const getDeployments = () => {
     let isSubscribed = true;
     return api
-    .getPRDeploymentList(
-      "<token>",
-      {},
-      {
-        project_id: currentProject.id,
-        cluster_id: currentCluster.id,
-        git_installation_id: git_installation_id,
-      }
-    )
-    .then(({ data }) => {
+      .getPRDeploymentList(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+        }
+      )
+      .then(({ data }) => {
         if (!isSubscribed) {
           return;
         }
@@ -111,44 +109,31 @@ const EnvironmentList = () => {
           throw Error("Data is not an array");
         }
 
-        data = data.forEach((d) => {
-          d.git_installation_id = git_installation_id;
-        })
-
         return Promise.resolve(data);
       })
-    .catch((err) => {
-      console.error(err);
-      if (isSubscribed) {
-        // setHasError(true);
-        setDeploymentList([]);
-      }
-      return Promise.resolve([]);
-    })  
-  }
+      .catch((err) => {
+        console.error(err);
+        if (isSubscribed) {
+          // setHasError(true);
+          setDeploymentList([]);
+        }
+        return Promise.resolve([]);
+      });
+  };
 
   useEffect(() => {
     let isSubscribed = true;
 
-    let gitInstallations = environmentList.map((e) => {
-      return e.git_installation_id
-    })
-
-    let uniqueIds = _.uniq(gitInstallations);
-
-    let promises = uniqueIds.map((git_installation_id) => {
-      return getDeployments(git_installation_id)
-    });
+    let promises = [getDeployments()];
 
-    Promise.all(promises).then((data) => {      
-      let result:PRDeployment[] = []
+    Promise.all(promises).then((data) => {
+      let result: PRDeployment[] = [];
       data.forEach((d) => {
-        result.concat(d)
+        result.concat(d);
       });
       setDeploymentList(result);
     });
 
-
     if (isSubscribed) {
       setIsLoading(false);
     }
@@ -184,11 +169,7 @@ const EnvironmentList = () => {
   }
 
   if (hasError) {
-    return(
-      <Placeholder>
-       Error
-      </Placeholder>
-    )
+    return <Placeholder>Error</Placeholder>;
   }
 
   if (!environmentList.length) {
@@ -203,13 +184,14 @@ const EnvironmentList = () => {
     if (!deploymentList.length) {
       return (
         <Placeholder>
-          No preview apps have been found. Open a PR to create a new preview app.
+          No preview apps have been found. Open a PR to create a new preview
+          app.
         </Placeholder>
       );
     }
 
     return deploymentList.map((d) => {
-      let repository = `${d.gh_repo_owner}/${d.gh_repo_name}`
+      let repository = `${d.gh_repo_owner}/${d.gh_repo_name}`;
       return (
         <EnvironmentCard key={d.id}>
           <DataContainer>
@@ -219,29 +201,27 @@ const EnvironmentList = () => {
             </PRName>
 
             <Flex>
-            <StatusContainer>
-              <Status>
-                <StatusDot status={d.status} />
-                {capitalize(d.status)}
-              </Status>
-            </StatusContainer>
-            <DeploymentImageContainer>
-              <DeploymentTypeIcon src={integrationList.repo.icon} />
-              <RepositoryName
-                onMouseOver={() => {
-                  setShowRepoTooltip(true);
-                }}
-                onMouseOut={() => {
-                  setShowRepoTooltip(false);
-                }}
-              >
-                {repository}
-              </RepositoryName>
-              {showRepoTooltip && <Tooltip>{repository}</Tooltip>}
-            </DeploymentImageContainer>
+              <StatusContainer>
+                <Status>
+                  <StatusDot status={d.status} />
+                  {capitalize(d.status)}
+                </Status>
+              </StatusContainer>
+              <DeploymentImageContainer>
+                <DeploymentTypeIcon src={integrationList.repo.icon} />
+                <RepositoryName
+                  onMouseOver={() => {
+                    setShowRepoTooltip(true);
+                  }}
+                  onMouseOut={() => {
+                    setShowRepoTooltip(false);
+                  }}
+                >
+                  {repository}
+                </RepositoryName>
+                {showRepoTooltip && <Tooltip>{repository}</Tooltip>}
+              </DeploymentImageContainer>
             </Flex>
-
-
           </DataContainer>
           <Flex>
             <RowButton
@@ -251,19 +231,15 @@ const EnvironmentList = () => {
               <i className="material-icons-outlined">info</i>
               Details
             </RowButton>
-            <RowButton 
-              to={d.subdomain} 
-              key={d.subdomain}
-              target="_blank"
-            >
+            <RowButton to={d.subdomain} key={d.subdomain} target="_blank">
               <i className="material-icons">open_in_new</i>
               View Live
             </RowButton>
           </Flex>
         </EnvironmentCard>
       );
-    }) 
-  }
+    });
+  };
 
   return (
     <Container>
@@ -274,9 +250,11 @@ const EnvironmentList = () => {
         >
           <i className="material-icons">add</i> Add Repository
         </Button>
-        <SettingsButton onClick={() => {
-            setCurrentModal("PreviewEnvSettingsModal", {})
-        }}>
+        <SettingsButton
+          onClick={() => {
+            setCurrentModal("PreviewEnvSettingsModal", {});
+          }}
+        >
           <i className="material-icons-outlined">settings</i>
           Configure
         </SettingsButton>
@@ -287,9 +265,7 @@ const EnvironmentList = () => {
           </SettingsText>
         </Settings> */}
       </ControlRow>
-      <EventsGrid>
-        {renderDeploymentList()}
-      </EventsGrid>
+      <EventsGrid>{renderDeploymentList()}</EventsGrid>
     </Container>
   );
 };
@@ -580,4 +556,4 @@ const Tooltip = styled.div`
       opacity: 1;
     }
   }
-`;
+`;

+ 28 - 29
dashboard/src/main/home/cluster-dashboard/dashboard/preview-environments/components/ConnectNewRepo.tsx

@@ -31,38 +31,37 @@ const ConnectNewRepo: React.FC = () => {
     git_repo_id: 0,
   });
 
-  useEffect(() => {
-  }, [repo])
+  useEffect(() => {}, [repo]);
 
   const { url } = useRouteMatch();
 
   const addRepo = () => {
-    let [owner, repoName] = repo.split('/')
-    setStatus('loading')
+    let [owner, repoName] = repo.split("/");
+    setStatus("loading");
     api
-    .createEnvironment(
-      "<token>",
-      {
-        name: 'Preview',
-        git_repo_name: repoName,
-        git_repo_owner: owner,
-      },
-      {
-        project_id: currentProject.id,
-        cluster_id: currentCluster.id,
-        git_installation_id: actionConfig.git_repo_id,
-      }
-    )
-    .then(() => {
-      setStatus('successful');
-      window.location.href = `${url}?selected_tab=preview_environments`
-    })
-    .catch((err) => {
-      err = JSON.stringify(err);
-      setStatus('error')
-      setCurrentError(err)
-    })
-  }
+      .createEnvironment(
+        "<token>",
+        {
+          name: "Preview",
+        },
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+          git_installation_id: actionConfig.git_repo_id,
+          git_repo_name: repoName,
+          git_repo_owner: owner,
+        }
+      )
+      .then(() => {
+        setStatus("successful");
+        window.location.href = `${url}?selected_tab=preview_environments`;
+      })
+      .catch((err) => {
+        err = JSON.stringify(err);
+        setStatus("error");
+        setCurrentError(err);
+      });
+  };
 
   return (
     <div>
@@ -78,8 +77,8 @@ const ConnectNewRepo: React.FC = () => {
       <RepoList
         actionConfig={actionConfig}
         setActionConfig={(a: ActionConfigType) => {
-          setActionConfig(a)
-          setRepo(a.git_repo)
+          setActionConfig(a);
+          setRepo(a.git_repo);
         }}
         readOnly={false}
       />

+ 74 - 68
dashboard/src/main/home/modals/PreviewEnvSettingsModal.tsx

@@ -11,13 +11,13 @@ import Helper from "components/form-components/Helper";
 import ConfirmOverlay from "../../../components/ConfirmOverlay";
 
 interface Environment {
-    id: Number,
-    project_id: number,
-    cluster_id: number,
-    git_installation_id: number,
-    name: string,
-    git_repo_owner: string,
-    git_repo_name: string,
+  id: Number;
+  project_id: number;
+  cluster_id: number;
+  git_installation_id: number;
+  name: string;
+  git_repo_owner: string;
+  git_repo_name: string;
 }
 
 const PreviewEnvSettingsModal = () => {
@@ -32,17 +32,21 @@ const PreviewEnvSettingsModal = () => {
 
   useEffect(() => {
     api
-      .listEnvironments("<token>", {}, {
+      .listEnvironments(
+        "<token>",
+        {},
+        {
           project_id: currentProject.id,
           cluster_id: currentCluster.id,
-      })
+        }
+      )
       .then(({ data }) => {
-        console.log('github account', data)
+        console.log("github account", data);
 
         if (!Array.isArray(data)) {
-            throw Error("Data is not an array");
-          }
-  
+          throw Error("Data is not an array");
+        }
+
         setAccessData(data);
         setAccessLoading(false);
       })
@@ -54,31 +58,33 @@ const PreviewEnvSettingsModal = () => {
 
   const handleDelete = () => {
     api
-    .deleteEnvironment("<token>", {
-        name: "preview",
-        git_repo_owner: selectedEnvironment.git_repo_owner,
-        git_repo_name: selectedEnvironment.git_repo_name,
-    }, {
-        project_id: currentProject.id,
-        cluster_id: currentCluster.id,
-        git_installation_id: selectedEnvironment.git_installation_id,
-    })
-    .then(() => {
+      .deleteEnvironment(
+        "<token>",
+        {
+          name: "preview",
+        },
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+          git_installation_id: selectedEnvironment.git_installation_id,
+          git_repo_owner: selectedEnvironment.git_repo_owner,
+          git_repo_name: selectedEnvironment.git_repo_name,
+        }
+      )
+      .then(() => {
         setSelectedEnvironment(null);
-    })
-    .catch((err) => {
-        setCurrentError(JSON.stringify(err))
-    })
-  }
+      })
+      .catch((err) => {
+        setCurrentError(JSON.stringify(err));
+      });
+  };
 
   return (
     <>
-    <ConfirmOverlay
+      <ConfirmOverlay
         show={selectedEnvironment != null}
-        message={
-          `Are you sure you want to disable preview environments in 
-          ${selectedEnvironment?.git_repo_owner}/${selectedEnvironment?.git_repo_name}?`
-        }
+        message={`Are you sure you want to disable preview environments in 
+          ${selectedEnvironment?.git_repo_owner}/${selectedEnvironment?.git_repo_name}?`}
         onYes={handleDelete}
         onNo={() => setSelectedEnvironment(null)}
       />
@@ -94,9 +100,7 @@ const PreviewEnvSettingsModal = () => {
         <>
           {accessError && (
             <ListWrapper>
-              <Helper>
-                No connected repositories found.
-              </Helper>
+              <Helper>No connected repositories found.</Helper>
             </ListWrapper>
           )}
 
@@ -108,9 +112,7 @@ const PreviewEnvSettingsModal = () => {
               </User>
               {accessData.length == 0 ? (
                 <ListWrapper>
-                  <Helper>
-                    No connected repositories found.
-                  </Helper>
+                  <Helper>No connected repositories found.</Helper>
                 </ListWrapper>
               ) : (
                 <>
@@ -118,15 +120,19 @@ const PreviewEnvSettingsModal = () => {
                     {accessData.map((e, i) => {
                       return (
                         <React.Fragment key={i}>
-                            <Row isLastItem={false}>
-                                <Flex>
-                                    <i className="material-icons">bookmark</i>
-                                    {`${e.git_repo_owner}/${e.git_repo_name}`}
-                                </Flex>
-                            <DisableButton onClick={() => {setSelectedEnvironment(e)}}>
-                                <i className="material-icons">delete</i>
+                          <Row isLastItem={false}>
+                            <Flex>
+                              <i className="material-icons">bookmark</i>
+                              {`${e.git_repo_owner}/${e.git_repo_name}`}
+                            </Flex>
+                            <DisableButton
+                              onClick={() => {
+                                setSelectedEnvironment(e);
+                              }}
+                            >
+                              <i className="material-icons">delete</i>
                             </DisableButton>
-                            </Row>
+                          </Row>
                         </React.Fragment>
                       );
                     })}
@@ -145,30 +151,30 @@ const PreviewEnvSettingsModal = () => {
 export default PreviewEnvSettingsModal;
 
 const DisableButton = styled.div`
-    margin-right: 13px;
-    cursor: pointer;
-    
-    > i {
-        margin-top: 5px;
-        font-size: 18px;
-        :hover {
-            color: #ffffff44;   
-        }
+  margin-right: 13px;
+  cursor: pointer;
+
+  > i {
+    margin-top: 5px;
+    font-size: 18px;
+    :hover {
+      color: #ffffff44;
     }
-`
+  }
+`;
 
 const Flex = styled.div`
-    display: flex;
-    justify-content: center;
-    align-items: center;
-
-    > i {
-        font-size: 17px;
-        margin-left: 10px;
-        margin-right: 12px;
-        color: #ffffff44;
-    }
-`
+  display: flex;
+  justify-content: center;
+  align-items: center;
+
+  > i {
+    font-size: 17px;
+    margin-left: 10px;
+    margin-right: 12px;
+    color: #ffffff44;
+  }
+`;
 
 const User = styled.div`
   margin-top: 14px;

+ 54 - 35
dashboard/src/shared/api.tsx

@@ -112,43 +112,55 @@ const createEmailVerification = baseApi<{}, {}>("POST", (pathParams) => {
 });
 
 const createEnvironment = baseApi<
-{
-  name: string;
-  git_repo_owner: string;
-  git_repo_name: string;
-},
-{
-  project_id: number;
-  cluster_id: number;
-  git_installation_id: number;
-}
+  {
+    name: string;
+  },
+  {
+    project_id: number;
+    cluster_id: number;
+    git_installation_id: number;
+    git_repo_owner: string;
+    git_repo_name: string;
+  }
 >("POST", (pathParams) => {
-  let { project_id, cluster_id, git_installation_id } = pathParams;
-  return `/api/projects/${project_id}/gitrepos/${git_installation_id}/clusters/${cluster_id}/environment`;
+  let {
+    project_id,
+    cluster_id,
+    git_installation_id,
+    git_repo_owner,
+    git_repo_name,
+  } = pathParams;
+  return `/api/projects/${project_id}/gitrepos/${git_installation_id}/${git_repo_owner}/${git_repo_name}/clusters/${cluster_id}/environment`;
 });
 
 const deleteEnvironment = baseApi<
-{
-  name: string;
-  git_repo_owner: string;
-  git_repo_name: string;
-},
-{
-  project_id: number;
-  cluster_id: number;
-  git_installation_id: number;
-}
+  {
+    name: string;
+  },
+  {
+    project_id: number;
+    cluster_id: number;
+    git_installation_id: number;
+    git_repo_owner: string;
+    git_repo_name: string;
+  }
 >("DELETE", (pathParams) => {
-  let { project_id, cluster_id, git_installation_id } = pathParams;
-  return `/api/projects/${project_id}/gitrepos/${git_installation_id}/clusters/${cluster_id}/environment`;
+  let {
+    project_id,
+    cluster_id,
+    git_installation_id,
+    git_repo_owner,
+    git_repo_name,
+  } = pathParams;
+  return `/api/projects/${project_id}/gitrepos/${git_installation_id}/${git_repo_owner}/${git_repo_name}/clusters/${cluster_id}/environment`;
 });
 
 const listEnvironments = baseApi<
-{},
-{
-  project_id: number;
-  cluster_id: number;
-}
+  {},
+  {
+    project_id: number;
+    cluster_id: number;
+  }
 >("GET", (pathParams) => {
   let { project_id, cluster_id } = pathParams;
   return `/api/projects/${project_id}/clusters/${cluster_id}/environments`;
@@ -331,26 +343,33 @@ const getPRDeploymentList = baseApi<
   {
     cluster_id: number;
     project_id: number;
-    git_installation_id: number;
   }
 >("GET", (pathParams) => {
-  const { cluster_id, project_id, git_installation_id } = pathParams;
+  const { cluster_id, project_id } = pathParams;
 
-  return `/api/projects/${project_id}/gitrepos/${git_installation_id}/clusters/${cluster_id}/deployments`;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/deployments`;
 });
 
 const getPRDeployment = baseApi<
   {
-    namespace: string,
+    namespace: string;
   },
   {
     cluster_id: number;
     project_id: number;
     git_installation_id: number;
+    git_repo_owner: string;
+    git_repo_name: string;
   }
 >("GET", (pathParams) => {
-  const { cluster_id, project_id, git_installation_id } = pathParams;
-  return `/api/projects/${project_id}/gitrepos/${git_installation_id}/clusters/${cluster_id}/deployment`;
+  const {
+    cluster_id,
+    project_id,
+    git_installation_id,
+    git_repo_owner,
+    git_repo_name,
+  } = pathParams;
+  return `/api/projects/${project_id}/gitrepos/${git_installation_id}/${git_repo_owner}/${git_repo_name}/clusters/${cluster_id}/deployment`;
 });
 
 const getNotificationConfig = baseApi<

+ 3 - 1
internal/integrations/ci/actions/steps.go

@@ -51,7 +51,7 @@ func getCreatePreviewEnvStep(serverURL, porterTokenSecretName string, projectID,
 			"token":           fmt.Sprintf("${{ secrets.%s }}", porterTokenSecretName),
 			"namespace":       fmt.Sprintf("pr-${{ github.event.pull_request.number }}-%s", repoName),
 			"pr_id":           "${{ github.event.pull_request.number }}",
-			"pr_name":		   "${{ github.event.pull_request.title }}",
+			"pr_name":         "${{ github.event.pull_request.title }}",
 			"installation_id": fmt.Sprintf("%d", gitInstallationID),
 			"branch":          "${{ github.head_ref }}",
 			"action_id":       "${{ github.run_id }}",
@@ -73,6 +73,8 @@ func getDeletePreviewEnvStep(serverURL, porterTokenSecretName string, projectID,
 			"token":           fmt.Sprintf("${{ secrets.%s }}", porterTokenSecretName),
 			"namespace":       fmt.Sprintf("pr-${{ github.event.pull_request.number }}-%s", repoName),
 			"installation_id": fmt.Sprintf("%d", gitInstallationID),
+			"repo_owner":      "${{ github.repository_owner }}",
+			"repo_name":       fmt.Sprintf("%s", repoName),
 		},
 		Timeout: 30,
 	}

+ 2 - 1
internal/repository/environment.go

@@ -4,11 +4,12 @@ import "github.com/porter-dev/porter/internal/models"
 
 type EnvironmentRepository interface {
 	CreateEnvironment(env *models.Environment) (*models.Environment, error)
-	ReadEnvironment(projectID, clusterID, gitInstallationID uint) (*models.Environment, error)
+	ReadEnvironment(projectID, clusterID, gitInstallationID uint, gitRepoOwner, gitRepoName string) (*models.Environment, error)
 	ListEnvironments(projectID, clusterID uint) ([]*models.Environment, error)
 	DeleteEnvironment(env *models.Environment) (*models.Environment, error)
 	CreateDeployment(deployment *models.Deployment) (*models.Deployment, error)
 	ReadDeployment(environmentID uint, namespace string) (*models.Deployment, error)
+	ListDeploymentsByCluster(projectID, clusterID uint) ([]*models.Deployment, error)
 	ListDeployments(environmentID uint) ([]*models.Deployment, error)
 	UpdateDeployment(deployment *models.Deployment) (*models.Deployment, error)
 	DeleteDeployment(deployment *models.Deployment) (*models.Deployment, error)

+ 16 - 2
internal/repository/gorm/environment.go

@@ -24,9 +24,13 @@ func (repo *EnvironmentRepository) CreateEnvironment(env *models.Environment) (*
 	return env, nil
 }
 
-func (repo *EnvironmentRepository) ReadEnvironment(projectID, clusterID, gitInstallationID uint) (*models.Environment, error) {
+func (repo *EnvironmentRepository) ReadEnvironment(projectID, clusterID, gitInstallationID uint, gitRepoOwner, gitRepoName string) (*models.Environment, error) {
 	env := &models.Environment{}
-	if err := repo.db.Order("id desc").Where("project_id = ? AND cluster_id = ? AND git_installation_id = ?", projectID, clusterID, gitInstallationID).First(&env).Error; err != nil {
+	if err := repo.db.Order("id desc").Where(
+		"project_id = ? AND cluster_id = ? AND git_installation_id = ? AND git_repo_owner = ? AND git_repo_name = ?",
+		projectID, clusterID, gitInstallationID,
+		gitRepoOwner, gitRepoName,
+	).First(&env).Error; err != nil {
 		return nil, err
 	}
 	return env, nil
@@ -72,6 +76,16 @@ func (repo *EnvironmentRepository) ReadDeployment(environmentID uint, namespace
 	return depl, nil
 }
 
+func (repo *EnvironmentRepository) ListDeploymentsByCluster(projectID, clusterID uint) ([]*models.Deployment, error) {
+	depls := make([]*models.Deployment, 0)
+
+	if err := repo.db.Order("id asc").Joins("join environments on project_id = ? AND cluster_id = ? AND environments.id = deployments.environment_id", projectID, clusterID).Find(&depls).Error; err != nil {
+		return nil, err
+	}
+
+	return depls, nil
+}
+
 func (repo *EnvironmentRepository) ListDeployments(environmentID uint) ([]*models.Deployment, error) {
 	depls := make([]*models.Deployment, 0)
 

+ 5 - 1
internal/repository/test/environment.go

@@ -19,7 +19,7 @@ func (repo *EnvironmentRepository) CreateEnvironment(env *models.Environment) (*
 	panic("unimplemented")
 }
 
-func (repo *EnvironmentRepository) ReadEnvironment(projectID, clusterID, gitInstallationID uint) (*models.Environment, error) {
+func (repo *EnvironmentRepository) ReadEnvironment(projectID, clusterID, gitInstallationID uint, gitRepoOwner, gitRepoName string) (*models.Environment, error) {
 	panic("unimplemented")
 }
 
@@ -43,6 +43,10 @@ func (repo *EnvironmentRepository) ReadDeployment(environmentID uint, namespace
 	panic("unimplemented")
 }
 
+func (repo *EnvironmentRepository) ListDeploymentsByCluster(projectID, clusterID uint) ([]*models.Deployment, error) {
+	panic("unimplemented")
+}
+
 func (repo *EnvironmentRepository) ListDeployments(environmentID uint) ([]*models.Deployment, error) {
 	panic("unimplemented")
 }