Kaynağa Gözat

Merge branch 'nico/por-652-porter-form-add-url-link-component' of github.com:porter-dev/porter into dev

jnfrati 3 yıl önce
ebeveyn
işleme
3bc77928ea

+ 7 - 0
.github/workflows/prerelease.yaml

@@ -577,6 +577,13 @@ jobs:
           git config user.email "support@porter.run"
 
           git diff --quiet --exit-code || git add . && git commit -m "Update to Porter GHA version ${{steps.tag_name.outputs.tag}}" && git push -f
+
+          git checkout test-preview-env
+          git merge main -m "Merge with main"
+
+          sed -i 's/TEST:v.*/TEST:${{ steps.tag_name.outputs.tag }}/g' porter.yaml
+
+          git diff --quiet --exit-code || git add . && git commit -m "Update to Porter GHA version ${{steps.tag_name.outputs.tag}}" && git push -f
   run-new-release-tests-workflows:
     name: Run new-release-tests Porter workflows
     runs-on: ubuntu-latest

+ 3 - 2
.gitignore

@@ -15,6 +15,7 @@ staging.sh
 *.key
 bin
 openapi.yaml
+.idea
 
 # Local docs directories
 /docs/.obsidian
@@ -37,8 +38,8 @@ openapi.yaml
 crash.log
 
 # Exclude all .tfvars files, which are likely to contain sentitive data, such as
-# password, private keys, and other secrets. These should not be part of version 
-# control as they are data points which are potentially sensitive and subject 
+# password, private keys, and other secrets. These should not be part of version
+# control as they are data points which are potentially sensitive and subject
 # to change depending on the environment.
 #
 *.tfvars

+ 1 - 1
Makefile

@@ -17,7 +17,7 @@ build-cli:
 	go build -ldflags="-w -s -X 'github.com/porter-dev/porter/cli/cmd/config.Version=${VERSION}'" -a -tags cli -o $(BINDIR)/porter ./cli
 
 build-cli-dev:
-	go build -tags cli -o $(BINDIR)/porter ./cli
+	go build -ldflags="-X 'github.com/porter-dev/porter/cli/cmd/config.Version=${VERSION}'" -tags cli -o $(BINDIR)/porter ./cli
 
 start-provisioner-dev: install setup-env-files
 	bash ./scripts/dev-environment/StartProvisionerServer.sh

+ 45 - 20
api/server/handlers/environment/create.go

@@ -1,6 +1,7 @@
 package environment
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"net/http"
@@ -88,7 +89,7 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 
 	// create incoming webhook
 	hook, _, err := client.Repositories.CreateHook(
-		r.Context(), owner, name, &github.Hook{
+		context.Background(), owner, name, &github.Hook{
 			Config: map[string]interface{}{
 				"url":          webhookURL,
 				"content_type": "json",
@@ -99,10 +100,9 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		},
 	)
 
-	if err != nil && !strings.Contains(err.Error(), "already exists on this repository") {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("error trying to create a new github repository webhook: %w", err), http.StatusConflict),
-		)
+	if err != nil && !strings.Contains(err.Error(), "already exists") {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, err),
+			http.StatusConflict))
 		return
 	}
 
@@ -111,6 +111,14 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	env, err = c.Repo().Environment().CreateEnvironment(env)
 
 	if err != nil {
+		_, deleteErr := client.Repositories.DeleteHook(context.Background(), owner, name, hook.GetID())
+
+		if deleteErr != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, deleteErr),
+				http.StatusConflict, "error creating environment"))
+			return
+		}
+
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
@@ -119,14 +127,44 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	jwt, err := token.GetTokenForAPI(user.ID, project.ID)
 
 	if err != nil {
-		c.deleteEnvAndReportError(w, r, env, err)
+		_, deleteErr := client.Repositories.DeleteHook(context.Background(), owner, name, hook.GetID())
+
+		if deleteErr != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, deleteErr),
+				http.StatusConflict, "error getting token for API while creating environment"))
+			return
+		}
+
+		_, deleteErr = c.Repo().Environment().DeleteEnvironment(env)
+
+		if deleteErr != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(deleteErr))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
 	encoded, err := jwt.EncodeToken(c.Config().TokenConf)
 
 	if err != nil {
-		c.deleteEnvAndReportError(w, r, env, err)
+		_, deleteErr := client.Repositories.DeleteHook(context.Background(), owner, name, hook.GetID())
+
+		if deleteErr != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, deleteErr),
+				http.StatusConflict, "error encoding token while creating environment"))
+			return
+		}
+
+		_, deleteErr = c.Repo().Environment().DeleteEnvironment(env)
+
+		if deleteErr != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(deleteErr))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -160,19 +198,6 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	c.WriteResult(w, r, env.ToEnvironmentType())
 }
 
-func (c *CreateEnvironmentHandler) deleteEnvAndReportError(
-	w http.ResponseWriter, r *http.Request, env *models.Environment, err error,
-) {
-	_, delErr := c.Repo().Environment().DeleteEnvironment(env)
-
-	if delErr != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(delErr))
-		return
-	}
-
-	c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-}
-
 func getGithubClientFromEnvironment(config *config.Config, env *models.Environment) (*github.Client, error) {
 	// get the github app client
 	ghAppId, err := strconv.Atoi(config.ServerConf.GithubAppID)

+ 45 - 28
api/server/handlers/environment/create_deployment.go

@@ -2,6 +2,7 @@ package environment
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net/http"
 
@@ -15,8 +16,11 @@ 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"
 )
 
+var errGithubAPI = errors.New("error communicating with the github API")
+
 type CreateDeploymentHandler struct {
 	handlers.PorterHandlerReadWriter
 	authz.KubernetesAgentGetter
@@ -54,6 +58,13 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	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("error creating deployment: no environment found")),
+			)
+			return
+		}
+
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
@@ -70,22 +81,21 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	prClosed, err := isGithubPRClosed(client, owner, name, int(request.PullRequestID))
 
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("error fetching details of github PR. Error: %w", err), http.StatusConflict,
-		))
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusConflict))
 		return
 	}
 
 	if prClosed {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("Github PR has been closed"),
-			http.StatusConflict))
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("cannot create deployment for closed github PR"), http.StatusConflict,
+		))
 		return
 	}
 
-	ghDeployment, err := createDeployment(client, env, request.PRBranchFrom, request.ActionID)
+	ghDeployment, err := createGithubDeployment(client, env, request.PRBranchFrom, request.ActionID)
 
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusConflict))
 		return
 	}
 
@@ -105,6 +115,20 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	})
 
 	if err != nil {
+		// try to delete the GitHub deployment
+		_, err = client.Repositories.DeleteDeployment(
+			context.Background(),
+			env.GitRepoOwner,
+			env.GitRepoName,
+			ghDeployment.GetID(),
+		)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, err),
+				http.StatusConflict))
+			return
+		}
+
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
@@ -127,7 +151,7 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	c.WriteResult(w, r, depl.ToDeploymentType())
 }
 
-func createDeployment(
+func createGithubDeployment(
 	client *github.Client,
 	env *models.Environment,
 	branchFrom string,
@@ -135,46 +159,39 @@ func createDeployment(
 ) (*github.Deployment, error) {
 	requiredContexts := []string{}
 
-	deploymentRequest := github.DeploymentRequest{
-		Ref:              github.String(branchFrom),
-		Environment:      github.String(env.Name),
-		AutoMerge:        github.Bool(false),
-		RequiredContexts: &requiredContexts,
-	}
-
 	deployment, _, err := client.Repositories.CreateDeployment(
 		context.Background(),
 		env.GitRepoOwner,
 		env.GitRepoName,
-		&deploymentRequest,
+		&github.DeploymentRequest{
+			Ref:              github.String(branchFrom),
+			Environment:      github.String(env.Name),
+			AutoMerge:        github.Bool(false),
+			RequiredContexts: &requiredContexts,
+		},
 	)
 
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("%v: %w", errGithubAPI, err)
 	}
 
 	depID := deployment.GetID()
 
 	// Create Deployment Status to indicate it's in progress
-
-	state := "in_progress"
-	log_url := fmt.Sprintf("https://github.com/%s/%s/actions/runs/%d", env.GitRepoOwner, env.GitRepoName, actionID)
-
-	deploymentStatusRequest := github.DeploymentStatusRequest{
-		State:  &state,
-		LogURL: &log_url, // link to actions tab
-	}
-
 	_, _, err = client.Repositories.CreateDeploymentStatus(
 		context.Background(),
 		env.GitRepoOwner,
 		env.GitRepoName,
 		depID,
-		&deploymentStatusRequest,
+		&github.DeploymentStatusRequest{
+			State: github.String("in_progress"),
+			LogURL: github.String(fmt.Sprintf("https://github.com/%s/%s/actions/runs/%d",
+				env.GitRepoOwner, env.GitRepoName, actionID)), // link to actions tab
+		},
 	)
 
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("%v: %w", errGithubAPI, err)
 	}
 
 	return deployment, nil

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

@@ -17,6 +17,7 @@ import (
 	"github.com/porter-dev/porter/internal/integrations/ci/actions"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models/integrations"
+	"gorm.io/gorm"
 )
 
 type DeleteEnvironmentHandler struct {
@@ -50,14 +51,11 @@ func (c *DeleteEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	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))
-		return
-	}
-
-	// delete Github actions files from the repo
-	client, err := getGithubClientFromEnvironment(c.Config(), env)
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("environment not found in cluster and project")))
+			return
+		}
 
-	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
@@ -92,6 +90,13 @@ func (c *DeleteEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
+	client, err := getGithubClientFromEnvironment(c.Config(), env)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
 	// FIXME: ignore the return status codes for now, should be fixed when we start returning all non-fatal errors
 	if ghWebhookID != 0 {
 		client.Repositories.DeleteHook(context.Background(), owner, name, ghWebhookID)
@@ -129,7 +134,7 @@ func (c *DeleteEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	if err != nil {
 		if errors.Is(err, actions.ErrProtectedBranch) {
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-				fmt.Errorf("We were unable to delete the Porter Preview Environment workflow files for this "+
+				fmt.Errorf("we were unable to delete the Porter Preview Environment workflow files for this "+
 					"repository as the default branch is protected. Please manually delete them."), http.StatusConflict,
 			))
 			return

+ 27 - 22
api/server/handlers/environment/delete_deployment.go

@@ -50,6 +50,11 @@ func (c *DeleteDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	depl, err := c.Repo().Environment().ReadDeploymentByID(project.ID, cluster.ID, deplID)
 
 	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("deployment id not found in cluster and project")))
+			return
+		}
+
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
@@ -85,41 +90,41 @@ func (c *DeleteDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		return
 	}
 
-	client, err := getGithubClientFromEnvironment(c.Config(), env)
+	depl.Status = types.DeploymentStatusInactive
+
+	// update the deployment to mark it inactive
+	depl, err = c.Repo().Environment().UpdateDeployment(depl)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	// Create new deployment status to indicate deployment is ready
-	state := "inactive"
-
-	deploymentStatusRequest := github.DeploymentStatusRequest{
-		State: &state,
-	}
-
-	_, _, err = client.Repositories.CreateDeploymentStatus(
-		context.Background(),
-		env.GitRepoOwner,
-		env.GitRepoName,
-		depl.GHDeploymentID,
-		&deploymentStatusRequest,
-	)
+	client, err := getGithubClientFromEnvironment(c.Config(), env)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	depl.Status = types.DeploymentStatusInactive
-
-	// update the deployment to mark it inactive
-	depl, err = c.Repo().Environment().UpdateDeployment(depl)
+	if depl.GHDeploymentID != 0 {
+		// set the GitHub deployment status to be inactive
+		_, _, err := client.Repositories.CreateDeploymentStatus(
+			context.Background(),
+			env.GitRepoOwner,
+			env.GitRepoName,
+			depl.GHDeploymentID,
+			&github.DeploymentStatusRequest{
+				State: github.String("inactive"),
+			},
+		)
 
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("%v: %w", errGithubAPI, err), http.StatusConflict,
+			))
+			return
+		}
 	}
 
 	c.WriteResult(w, r, depl.ToDeploymentType())

+ 14 - 7
api/server/handlers/environment/enable_pull_request.go

@@ -1,6 +1,7 @@
 package environment
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 	"strconv"
@@ -14,6 +15,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
 )
 
 type EnablePullRequestHandler struct {
@@ -44,6 +46,11 @@ func (c *EnablePullRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	env, err := c.Repo().Environment().ReadEnvironmentByOwnerRepoName(project.ID, cluster.ID, request.RepoOwner, request.RepoName)
 
 	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("environment not found in cluster and project")))
+			return
+		}
+
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
@@ -59,12 +66,13 @@ func (c *EnablePullRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	pr, _, err := client.PullRequests.Get(r.Context(), env.GitRepoOwner, env.GitRepoName, int(request.Number))
 
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, err),
+			http.StatusConflict))
 		return
 	}
 
 	if pr.GetState() == "closed" {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("Github PR has been closed"),
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("cannot enable deployment for closed PR"),
 			http.StatusConflict))
 		return
 	}
@@ -86,17 +94,17 @@ func (c *EnablePullRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		if ghResp.StatusCode == 404 {
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
 				fmt.Errorf(
-					"Please make sure the preview environment workflow files are present in PR branch %s and are up to"+
+					"please make sure the preview environment workflow files are present in PR branch %s and are up to"+
 						" date with the default branch", request.BranchFrom,
-				), 404),
+				), http.StatusConflict),
 			)
 			return
 		} else if ghResp.StatusCode == 422 {
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
 				fmt.Errorf(
-					"Please make sure the workflow files in PR branch %s are up to date with the default branch",
+					"please make sure the workflow files in PR branch %s are up to date with the default branch",
 					request.BranchFrom,
-				), 422),
+				), http.StatusConflict),
 			)
 			return
 		}
@@ -141,5 +149,4 @@ func (c *EnablePullRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
-
 }

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

@@ -264,7 +264,7 @@ func isGithubPRClosed(
 	)
 
 	if err != nil {
-		return false, err
+		return false, fmt.Errorf("%v: %w", errGithubAPI, err)
 	}
 
 	return ghPR.GetState() == "closed", nil

+ 4 - 9
api/server/handlers/environment/finalize_deployment_with_errors.go

@@ -61,7 +61,7 @@ func (c *FinalizeDeploymentWithErrorsHandler) ServeHTTP(w http.ResponseWriter, r
 
 	if err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("no environment found")))
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("environment not found in cluster and project")))
 			return
 		}
 
@@ -85,9 +85,7 @@ func (c *FinalizeDeploymentWithErrorsHandler) ServeHTTP(w http.ResponseWriter, r
 	client, err := getGithubClientFromEnvironment(c.Config(), env)
 
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("unable to get github client: %w", err), http.StatusConflict,
-		))
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -95,15 +93,12 @@ func (c *FinalizeDeploymentWithErrorsHandler) ServeHTTP(w http.ResponseWriter, r
 	prClosed, err := isGithubPRClosed(client, owner, name, int(depl.PullRequestID))
 
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("error fetching details of github PR for deployment ID: %d. Error: %w",
-				depl.ID, err), http.StatusConflict,
-		))
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusConflict))
 		return
 	}
 
 	if prClosed {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("Github PR has been closed"),
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("github PR has been closed"),
 			http.StatusConflict))
 		return
 	}

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

@@ -1,6 +1,7 @@
 package environment
 
 import (
+	"fmt"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -31,7 +32,9 @@ func (c *ListEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	envs, err := c.Repo().Environment().ListEnvironments(project.ID, cluster.ID)
 
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("error listing environments"), http.StatusInternalServerError, err.Error(),
+		))
 		return
 	}
 
@@ -43,7 +46,10 @@ func (c *ListEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		depls, err := c.Repo().Environment().ListDeployments(env.ID)
 
 		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("error listing environments: error listing deployments for environment ID %d", env.ID),
+				http.StatusInternalServerError, err.Error(),
+			))
 			return
 		}
 

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

@@ -89,7 +89,7 @@ func (c *UpdateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		return
 	}
 
-	ghDeployment, err := createDeployment(client, env, request.PRBranchFrom, request.ActionID)
+	ghDeployment, err := createGithubDeployment(client, env, request.PRBranchFrom, request.ActionID)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 1 - 1
api/server/router/git_installation.go

@@ -115,7 +115,7 @@ func getGitInstallationRoutes(
 
 	if config.ServerConf.GithubIncomingWebhookSecret != "" {
 
-		// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id} ->
+		// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/environment ->
 		// environment.NewCreateEnvironmentHandler
 		createEnvironmentEndpoint := factory.NewAPIEndpoint(
 			&types.APIRequestMetadata{

+ 63 - 46
cli/cmd/apply.go

@@ -25,7 +25,7 @@ import (
 	"github.com/porter-dev/switchboard/pkg/models"
 	"github.com/porter-dev/switchboard/pkg/parser"
 	switchboardTypes "github.com/porter-dev/switchboard/pkg/types"
-	"github.com/porter-dev/switchboard/pkg/worker"
+	switchboardWorker "github.com/porter-dev/switchboard/pkg/worker"
 	"github.com/rs/zerolog"
 	"github.com/spf13/cobra"
 )
@@ -80,25 +80,27 @@ func init() {
 	applyCmd.MarkFlagRequired("file")
 }
 
-func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
 	fileBytes, err := ioutil.ReadFile(porterYAML)
+
 	if err != nil {
-		return err
+		return fmt.Errorf("error reading porter.yaml: %w", err)
 	}
 
 	resGroup, err := parser.ParseRawBytes(fileBytes)
+
 	if err != nil {
-		return err
+		return fmt.Errorf("error parsing porter.yaml: %w", err)
 	}
 
 	basePath, err := os.Getwd()
 
 	if err != nil {
-		return err
+		return fmt.Errorf("error getting working directory: %w", err)
 	}
 
-	worker := worker.NewWorker()
-	worker.RegisterDriver("deploy", NewPorterDriver)
+	worker := switchboardWorker.NewWorker()
+	worker.RegisterDriver("deploy", NewDeployDriver)
 	worker.RegisterDriver("build-image", preview.NewBuildDriver)
 	worker.RegisterDriver("push-image", preview.NewPushDriver)
 	worker.RegisterDriver("update-config", preview.NewUpdateConfigDriver)
@@ -118,7 +120,7 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []str
 		deploymentHook, err := NewDeploymentHook(client, resGroup, deplNamespace)
 
 		if err != nil {
-			return err
+			return fmt.Errorf("error creating deployment hook: %w", err)
 		}
 
 		worker.RegisterHook("deployment", deploymentHook)
@@ -191,7 +193,7 @@ type ApplicationConfig struct {
 	Values map[string]interface{}
 }
 
-type Driver struct {
+type DeployDriver struct {
 	source      *preview.Source
 	target      *preview.Target
 	output      map[string]interface{}
@@ -199,21 +201,23 @@ type Driver struct {
 	logger      *zerolog.Logger
 }
 
-func NewPorterDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
-	driver := &Driver{
+func NewDeployDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
+	driver := &DeployDriver{
 		lookupTable: opts.DriverLookupTable,
 		logger:      opts.Logger,
 		output:      make(map[string]interface{}),
 	}
 
-	source, err := preview.GetSource(resource.Source)
+	source, err := preview.GetSource(resource.Name, resource.Source)
+
 	if err != nil {
 		return nil, err
 	}
 
 	driver.source = source
 
-	target, err := preview.GetTarget(resource.Target)
+	target, err := preview.GetTarget(resource.Name, resource.Target)
+
 	if err != nil {
 		return nil, err
 	}
@@ -223,16 +227,16 @@ func NewPorterDriver(resource *models.Resource, opts *drivers.SharedDriverOpts)
 	return driver, nil
 }
 
-func (d *Driver) ShouldApply(resource *models.Resource) bool {
+func (d *DeployDriver) ShouldApply(_ *models.Resource) bool {
 	return true
 }
 
-func (d *Driver) Apply(resource *models.Resource) (*models.Resource, error) {
+func (d *DeployDriver) Apply(resource *models.Resource) (*models.Resource, error) {
 	client := config.GetAPIClient()
 	name := resource.Name
 
 	if name == "" {
-		return nil, fmt.Errorf("empty app name")
+		return nil, fmt.Errorf("empty resource name")
 	}
 
 	_, err := client.GetRelease(
@@ -257,11 +261,11 @@ func (d *Driver) Apply(resource *models.Resource) (*models.Resource, error) {
 }
 
 // Simple apply for addons
-func (d *Driver) applyAddon(resource *models.Resource, client *api.Client, shouldCreate bool) (*models.Resource, error) {
+func (d *DeployDriver) applyAddon(resource *models.Resource, client *api.Client, shouldCreate bool) (*models.Resource, error) {
 	addonConfig, err := d.getAddonConfig(resource)
 
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("error getting addon config for resource %s: %w", resource.Name, err)
 	}
 
 	if shouldCreate {
@@ -282,13 +286,13 @@ func (d *Driver) applyAddon(resource *models.Resource, client *api.Client, shoul
 		)
 
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("error creating addon from resource %s: %w", resource.Name, err)
 		}
 	} else {
 		bytes, err := json.Marshal(addonConfig)
 
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("error marshalling addon config from resource %s: %w", resource.Name, err)
 		}
 
 		err = client.UpgradeRelease(
@@ -303,7 +307,7 @@ func (d *Driver) applyAddon(resource *models.Resource, client *api.Client, shoul
 		)
 
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("error updating addon from resource %s: %w", resource.Name, err)
 		}
 	}
 
@@ -314,7 +318,11 @@ func (d *Driver) applyAddon(resource *models.Resource, client *api.Client, shoul
 	return resource, nil
 }
 
-func (d *Driver) applyApplication(resource *models.Resource, client *api.Client, shouldCreate bool) (*models.Resource, error) {
+func (d *DeployDriver) applyApplication(resource *models.Resource, client *api.Client, shouldCreate bool) (*models.Resource, error) {
+	if resource == nil {
+		return nil, fmt.Errorf("nil resource")
+	}
+
 	appConfig, err := d.getApplicationConfig(resource)
 
 	if err != nil {
@@ -324,25 +332,32 @@ func (d *Driver) applyApplication(resource *models.Resource, client *api.Client,
 	method := appConfig.Build.Method
 
 	if method != "pack" && method != "docker" && method != "registry" {
-		return nil, fmt.Errorf("method should either be \"docker\", \"pack\" or \"registry\"")
+		return nil, fmt.Errorf("for resource %s, config.build.method should either be \"docker\", \"pack\" or \"registry\"",
+			resource.Name)
 	}
 
 	fullPath, err := filepath.Abs(appConfig.Build.Context)
 
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("for resource %s, error getting absolute path for config.build.context: %w", resource.Name,
+			err)
 	}
 
 	tag := os.Getenv("PORTER_TAG")
 
 	if tag == "" {
+		color.New(color.FgYellow).Printf("for resource %s, since PORTER_TAG is not set, the Docker image tag will default to"+
+			" the git repo SHA", resource.Name)
+
 		commit, err := git.LastCommit()
 
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("for resource %s, error getting last git commit: %w", resource.Name, err)
 		}
 
 		tag = commit.Sha[:7]
+
+		color.New(color.FgYellow).Printf("for resource %s, using tag %s\n", resource.Name, tag)
 	}
 
 	// if the method is registry and a tag is defined, we use the provided tag
@@ -383,16 +398,16 @@ func (d *Driver) applyApplication(resource *models.Resource, client *api.Client,
 		resource, err = d.createApplication(resource, client, sharedOpts, appConfig)
 
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("error creating app from resource %s: %w", resource.Name, err)
 		}
 	} else if !appConfig.OnlyCreate {
 		resource, err = d.updateApplication(resource, client, sharedOpts, appConfig)
 
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("error updating application from resource %s: %w", resource.Name, err)
 		}
 	} else {
-		color.New(color.FgYellow).Printf("Skipping creation for %s as onlyCreate is set to true\n", resource.Name)
+		color.New(color.FgYellow).Printf("Skipping creation for resource %s as onlyCreate is set to true\n", resource.Name)
 	}
 
 	if err = d.assignOutput(resource, client); err != nil {
@@ -430,14 +445,14 @@ func (d *Driver) applyApplication(resource *models.Resource, client *api.Client,
 	return resource, err
 }
 
-func (d *Driver) createApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *ApplicationConfig) (*models.Resource, error) {
+func (d *DeployDriver) createApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *ApplicationConfig) (*models.Resource, error) {
 	// create new release
 	color.New(color.FgGreen).Printf("Creating %s release: %s\n", d.source.Name, resource.Name)
 
 	regList, err := client.ListRegistries(context.Background(), d.target.Project)
 
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("for resource %s, error listing registries: %w", resource.Name, err)
 	}
 
 	var registryURL string
@@ -448,6 +463,8 @@ func (d *Driver) createApplication(resource *models.Resource, client *api.Client
 		registryURL = (*regList)[0].URL
 	}
 
+	color.New(color.FgBlue).Printf("for resource %s, using registry %s\n", resource.Name, registryURL)
+
 	// attempt to get repo suffix from environment variables
 	var repoSuffix string
 
@@ -514,7 +531,7 @@ func (d *Driver) createApplication(resource *models.Resource, client *api.Client
 	return resource, handleSubdomainCreate(subdomain, err)
 }
 
-func (d *Driver) updateApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *ApplicationConfig) (*models.Resource, error) {
+func (d *DeployDriver) updateApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *ApplicationConfig) (*models.Resource, error) {
 	color.New(color.FgGreen).Println("Updating existing release:", resource.Name)
 
 	if len(appConf.Build.Env) > 0 {
@@ -580,7 +597,7 @@ func (d *Driver) updateApplication(resource *models.Resource, client *api.Client
 	return resource, nil
 }
 
-func (d *Driver) assignOutput(resource *models.Resource, client *api.Client) error {
+func (d *DeployDriver) assignOutput(resource *models.Resource, client *api.Client) error {
 	release, err := client.GetRelease(
 		context.Background(),
 		d.target.Project,
@@ -598,11 +615,11 @@ func (d *Driver) assignOutput(resource *models.Resource, client *api.Client) err
 	return nil
 }
 
-func (d *Driver) Output() (map[string]interface{}, error) {
+func (d *DeployDriver) Output() (map[string]interface{}, error) {
 	return d.output, nil
 }
 
-func (d *Driver) getApplicationConfig(resource *models.Resource) (*ApplicationConfig, error) {
+func (d *DeployDriver) getApplicationConfig(resource *models.Resource) (*ApplicationConfig, error) {
 	populatedConf, err := drivers.ConstructConfig(&drivers.ConstructConfigOpts{
 		RawConf:      resource.Config,
 		LookupTable:  *d.lookupTable,
@@ -613,9 +630,9 @@ func (d *Driver) getApplicationConfig(resource *models.Resource) (*ApplicationCo
 		return nil, err
 	}
 
-	config := &ApplicationConfig{}
+	appConf := &ApplicationConfig{}
 
-	err = mapstructure.Decode(populatedConf, config)
+	err = mapstructure.Decode(populatedConf, appConf)
 
 	if err != nil {
 		return nil, err
@@ -623,13 +640,13 @@ func (d *Driver) getApplicationConfig(resource *models.Resource) (*ApplicationCo
 
 	if _, ok := resource.Config["waitForJob"]; !ok && d.source.Name == "job" {
 		// default to true and wait for the job to finish
-		config.WaitForJob = true
+		appConf.WaitForJob = true
 	}
 
-	return config, nil
+	return appConf, nil
 }
 
-func (d *Driver) getAddonConfig(resource *models.Resource) (map[string]interface{}, error) {
+func (d *DeployDriver) getAddonConfig(resource *models.Resource) (map[string]interface{}, error) {
 	return drivers.ConstructConfig(&drivers.ConstructConfigOpts{
 		RawConf:      resource.Config,
 		LookupTable:  *d.lookupTable,
@@ -974,21 +991,21 @@ func (t *CloneEnvGroupHook) PreApply() error {
 			continue
 		}
 
-		config := &ApplicationConfig{}
+		appConf := &ApplicationConfig{}
 
-		err := mapstructure.Decode(res.Config, &config)
+		err := mapstructure.Decode(res.Config, &appConf)
 		if err != nil {
 			continue
 		}
 
-		if config != nil && len(config.EnvGroups) > 0 {
-			target, err := preview.GetTarget(res.Target)
+		if appConf != nil && len(appConf.EnvGroups) > 0 {
+			target, err := preview.GetTarget(res.Name, res.Target)
 
 			if err != nil {
 				return err
 			}
 
-			for _, group := range config.EnvGroups {
+			for _, group := range appConf.EnvGroups {
 				if group.Name == "" {
 					return fmt.Errorf("env group name cannot be empty")
 				}
@@ -1051,7 +1068,7 @@ func (t *CloneEnvGroupHook) OnConsolidatedErrors(map[string]error) {}
 func getReleaseName(res *switchboardTypes.Resource) string {
 	// can ignore the error because this method is called once
 	// GetTarget has alrealy been called and validated previously
-	target, _ := preview.GetTarget(res.Target)
+	target, _ := preview.GetTarget(res.Name, res.Target)
 
 	if target.AppName != "" {
 		return target.AppName
@@ -1063,7 +1080,7 @@ func getReleaseName(res *switchboardTypes.Resource) string {
 func getReleaseType(res *switchboardTypes.Resource) string {
 	// can ignore the error because this method is called once
 	// GetSource has alrealy been called and validated previously
-	source, _ := preview.GetSource(res.Source)
+	source, _ := preview.GetSource(res.Name, res.Source)
 
 	if source != nil && source.Name != "" {
 		return source.Name

+ 2 - 2
cli/cmd/preview/build_image_driver.go

@@ -49,14 +49,14 @@ func NewBuildDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (
 		output:      make(map[string]interface{}),
 	}
 
-	source, err := GetSource(resource.Source)
+	source, err := GetSource(resource.Name, resource.Source)
 	if err != nil {
 		return nil, err
 	}
 
 	driver.source = source
 
-	target, err := GetTarget(resource.Target)
+	target, err := GetTarget(resource.Name, resource.Target)
 	if err != nil {
 		return nil, err
 	}

+ 1 - 1
cli/cmd/preview/env_group_driver.go

@@ -29,7 +29,7 @@ func NewEnvGroupDriver(resource *models.Resource, opts *drivers.SharedDriverOpts
 		output:      make(map[string]interface{}),
 	}
 
-	target, err := GetTarget(resource.Target)
+	target, err := GetTarget(resource.Name, resource.Target)
 
 	if err != nil {
 		return nil, err

+ 1 - 1
cli/cmd/preview/push_image_driver.go

@@ -35,7 +35,7 @@ func NewPushDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (d
 		output:      make(map[string]interface{}),
 	}
 
-	target, err := GetTarget(resource.Target)
+	target, err := GetTarget(resource.Name, resource.Target)
 	if err != nil {
 		return nil, err
 	}

+ 2 - 2
cli/cmd/preview/update_config_driver.go

@@ -50,14 +50,14 @@ func NewUpdateConfigDriver(resource *models.Resource, opts *drivers.SharedDriver
 		output:      make(map[string]interface{}),
 	}
 
-	source, err := GetSource(resource.Source)
+	source, err := GetSource(resource.Name, resource.Source)
 	if err != nil {
 		return nil, err
 	}
 
 	driver.source = source
 
-	target, err := GetTarget(resource.Target)
+	target, err := GetTarget(resource.Name, resource.Target)
 	if err != nil {
 		return nil, err
 	}

+ 17 - 14
cli/cmd/preview/utils.go

@@ -25,7 +25,7 @@ type Target struct {
 	Namespace string
 }
 
-func GetSource(input map[string]interface{}) (*Source, error) {
+func GetSource(resourceName string, input map[string]interface{}) (*Source, error) {
 	output := &Source{}
 
 	// first read from env vars
@@ -38,21 +38,21 @@ func GetSource(input map[string]interface{}) (*Source, error) {
 		if name, ok := input["name"]; ok {
 			nameVal, ok := name.(string)
 			if !ok {
-				return nil, fmt.Errorf("invalid name provided")
+				return nil, fmt.Errorf("error parsing source for resource '%s': invalid name provided", resourceName)
 			}
 			output.Name = nameVal
 		}
 	}
 
 	if output.Name == "" {
-		return nil, fmt.Errorf("source name required")
+		return nil, fmt.Errorf("error parsing source for resource '%s': source name required", resourceName)
 	}
 
 	if output.Repo == "" {
 		if repo, ok := input["repo"]; ok {
 			repoVal, ok := repo.(string)
 			if !ok {
-				return nil, fmt.Errorf("invalid repo provided")
+				return nil, fmt.Errorf("error parsing source for resource '%s': invalid repo provided", resourceName)
 			}
 			output.Repo = repoVal
 		}
@@ -62,7 +62,7 @@ func GetSource(input map[string]interface{}) (*Source, error) {
 		if version, ok := input["version"]; ok {
 			versionVal, ok := version.(string)
 			if !ok {
-				return nil, fmt.Errorf("invalid version provided")
+				return nil, fmt.Errorf("error parsing source for resource '%s': invalid version provided", resourceName)
 			}
 			output.Version = versionVal
 		}
@@ -97,7 +97,8 @@ func GetSource(input map[string]interface{}) (*Source, error) {
 			return output, nil
 		}
 
-		return nil, fmt.Errorf("source does not exist in any repo")
+		return nil, fmt.Errorf("error parsing source for resource '%s': source does not exist in "+
+			"'https://charts.getporter.dev' or 'https://chart-addons.getporter.dev'", resourceName)
 	} else {
 		// we look in the passed-in repo
 		values, err := existsInRepo(output.Name, output.Version, output.Repo)
@@ -108,17 +109,18 @@ func GetSource(input map[string]interface{}) (*Source, error) {
 		}
 	}
 
-	return nil, fmt.Errorf("source '%s' does not exist in repo '%s'", output.Name, output.Repo)
+	return nil, fmt.Errorf("error parsing source for resource '%s': source '%s' does not exist in repo '%s'",
+		resourceName, output.Name, output.Repo)
 }
 
-func GetTarget(input map[string]interface{}) (*Target, error) {
+func GetTarget(resourceName string, input map[string]interface{}) (*Target, error) {
 	output := &Target{}
 
 	// first read from env vars
 	if projectEnv := os.Getenv("PORTER_PROJECT"); projectEnv != "" {
 		project, err := strconv.Atoi(projectEnv)
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("error parsing target for resource '%s': %w", resourceName, err)
 		}
 		output.Project = uint(project)
 	}
@@ -126,7 +128,7 @@ func GetTarget(input map[string]interface{}) (*Target, error) {
 	if clusterEnv := os.Getenv("PORTER_CLUSTER"); clusterEnv != "" {
 		cluster, err := strconv.Atoi(clusterEnv)
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("error parsing target for resource '%s': %w", resourceName, err)
 		}
 		output.Cluster = uint(cluster)
 	}
@@ -138,7 +140,7 @@ func GetTarget(input map[string]interface{}) (*Target, error) {
 		if project, ok := input["project"]; ok {
 			projectVal, ok := project.(uint)
 			if !ok {
-				return nil, fmt.Errorf("project value must be an integer")
+				return nil, fmt.Errorf("error parsing target for resource '%s': project value must be an integer", resourceName)
 			}
 			output.Project = projectVal
 		}
@@ -148,7 +150,8 @@ func GetTarget(input map[string]interface{}) (*Target, error) {
 		if cluster, ok := input["cluster"]; ok {
 			clusterVal, ok := cluster.(uint)
 			if !ok {
-				return nil, fmt.Errorf("cluster value must be an integer")
+				return nil, fmt.Errorf("error parsing target for resource '%s': cluster value must be an integer",
+					resourceName)
 			}
 			output.Cluster = clusterVal
 		}
@@ -158,7 +161,7 @@ func GetTarget(input map[string]interface{}) (*Target, error) {
 		if namespace, ok := input["namespace"]; ok {
 			namespaceVal, ok := namespace.(string)
 			if !ok {
-				return nil, fmt.Errorf("invalid namespace provided")
+				return nil, fmt.Errorf("error parsing target for resource '%s': invalid namespace provided", resourceName)
 			}
 			output.Namespace = namespaceVal
 		}
@@ -167,7 +170,7 @@ func GetTarget(input map[string]interface{}) (*Target, error) {
 	if appName, ok := input["app_name"]; ok {
 		appNameVal, ok := appName.(string)
 		if !ok {
-			return nil, fmt.Errorf("invalid app_name provided")
+			return nil, fmt.Errorf("error parsing target for resource '%s': invalid app_name provided", resourceName)
 		}
 		output.AppName = appNameVal
 	}

+ 4 - 3
cmd/app/main.go

@@ -116,9 +116,10 @@ func initData(conf *config.Config) error {
 			l.Debug().Msg("default cluster not found: attempting creation")
 
 			_, err = conf.Repo.Cluster().CreateCluster(&models.Cluster{
-				Name:          defaultClusterName,
-				AuthMechanism: models.InCluster,
-				ProjectID:     1,
+				Name:                defaultClusterName,
+				AuthMechanism:       models.InCluster,
+				ProjectID:           1,
+				MonitorHelmReleases: true,
 			})
 
 			if err != nil {

+ 3 - 2
provisioner/server/handlers/state/create_resource.go

@@ -303,8 +303,9 @@ func createCluster(config *config.Config, infra *models.Infra, operation *models
 
 func getNewCluster(infra *models.Infra) *models.Cluster {
 	res := &models.Cluster{
-		ProjectID: infra.ProjectID,
-		InfraID:   infra.ID,
+		ProjectID:           infra.ProjectID,
+		InfraID:             infra.ID,
+		MonitorHelmReleases: true,
 	}
 
 	switch infra.Kind {