Browse Source

Merge branch 'master' of github.com:porter-dev/porter into nafees/preview-env-improvements

jnfrati 3 years ago
parent
commit
2878b50246
43 changed files with 486 additions and 572 deletions
  1. 7 0
      .github/workflows/prerelease.yaml
  2. 3 2
      .gitignore
  3. 1 1
      Makefile
  4. 45 20
      api/server/handlers/environment/create.go
  5. 45 28
      api/server/handlers/environment/create_deployment.go
  6. 13 8
      api/server/handlers/environment/delete.go
  7. 27 22
      api/server/handlers/environment/delete_deployment.go
  8. 14 7
      api/server/handlers/environment/enable_pull_request.go
  9. 1 1
      api/server/handlers/environment/finalize_deployment.go
  10. 4 9
      api/server/handlers/environment/finalize_deployment_with_errors.go
  11. 8 2
      api/server/handlers/environment/list.go
  12. 1 1
      api/server/handlers/environment/update_deployment.go
  13. 2 11
      api/server/handlers/stack/create.go
  14. 0 64
      api/server/handlers/stack/update_stack.go
  15. 1 1
      api/server/router/git_installation.go
  16. 1 57
      api/server/router/v1/stack.go
  17. 0 10
      api/types/stacks.go
  18. 63 46
      cli/cmd/apply.go
  19. 2 2
      cli/cmd/preview/build_image_driver.go
  20. 1 1
      cli/cmd/preview/env_group_driver.go
  21. 1 1
      cli/cmd/preview/push_image_driver.go
  22. 2 2
      cli/cmd/preview/update_config_driver.go
  23. 17 14
      cli/cmd/preview/utils.go
  24. 4 3
      cmd/app/main.go
  25. 8 0
      dashboard/src/components/porter-form/FormDebugger.tsx
  26. 4 0
      dashboard/src/components/porter-form/PorterForm.tsx
  27. 94 0
      dashboard/src/components/porter-form/field-components/UrlLink.tsx
  28. 11 2
      dashboard/src/components/porter-form/types.ts
  29. 3 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  30. 3 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx
  31. 1 5
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/ExpandedStack.tsx
  32. 72 111
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_SourceConfig.tsx
  33. 5 67
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Settings.tsx
  34. 3 21
      dashboard/src/main/home/cluster-dashboard/stacks/launch/SelectSource.tsx
  35. 7 5
      dashboard/src/main/home/cluster-dashboard/stacks/launch/Store.tsx
  36. 0 2
      dashboard/src/main/home/cluster-dashboard/stacks/types.ts
  37. 0 17
      dashboard/src/shared/api.tsx
  38. 8 13
      internal/models/stack.go
  39. 0 8
      internal/repository/gorm/stack.go
  40. 0 1
      internal/repository/stack.go
  41. 0 4
      internal/repository/test/stack.go
  42. 1 1
      internal/stacks/helpers.go
  43. 3 2
      provisioner/server/handlers/state/create_resource.go

+ 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))

+ 2 - 11
api/server/handlers/stack/create.go

@@ -261,21 +261,12 @@ func getSourceConfigModels(sourceConfigs []*types.CreateStackSourceConfigRequest
 				return nil, err
 			}
 
-			newSourceConfig := &models.StackSourceConfig{
+			res = append(res, models.StackSourceConfig{
 				UID:          uid,
 				Name:         sourceConfig.Name,
 				ImageRepoURI: sourceConfig.ImageRepoURI,
 				ImageTag:     sourceConfig.ImageTag,
-			}
-
-			// If the source config had a source config ID then we need to copy it over
-			if sourceConfig.StableSourceConfigID != "" {
-				newSourceConfig.StableSourceConfigID = sourceConfig.StableSourceConfigID
-			} else {
-				newSourceConfig.StableSourceConfigID = string(uid)
-			}
-
-			res = append(res, *newSourceConfig)
+			})
 		}
 	}
 

+ 0 - 64
api/server/handlers/stack/update_stack.go

@@ -1,64 +0,0 @@
-package stack
-
-import (
-	"fmt"
-	"net/http"
-
-	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/shared"
-	"github.com/porter-dev/porter/api/server/shared/apierrors"
-	"github.com/porter-dev/porter/api/server/shared/config"
-	"github.com/porter-dev/porter/api/types"
-	"github.com/porter-dev/porter/internal/models"
-)
-
-type StackUpdateStack struct {
-	handlers.PorterHandlerReadWriter
-}
-
-func NewStackUpdateStackHandler(
-	config *config.Config,
-	reader shared.RequestDecoderValidator,
-	writer shared.ResultWriter,
-) *StackUpdateStack {
-	return &StackUpdateStack{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, reader, writer),
-	}
-}
-
-func (p *StackUpdateStack) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
-	stack, _ := r.Context().Value(types.StackScope).(*models.Stack)
-
-	req := &types.UpdateStackRequest{}
-
-	if ok := p.DecodeAndValidate(w, r, req); !ok {
-		return
-	}
-
-	if len(stack.Revisions) == 0 {
-		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("no stack revisions exist"), http.StatusBadRequest,
-		))
-		return
-	}
-
-	stack, err := p.Repo().Stack().ReadStackByID(proj.ID, stack.ID)
-
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	// Update stack name
-	stack.Name = req.Name
-
-	newStack, err := p.Repo().Stack().UpdateStack(stack)
-
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	p.WriteResult(w, r, newStack)
-}

+ 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{

+ 1 - 57
api/server/router/v1/stack.go

@@ -9,7 +9,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 )
 
-// swagger:parameters getStack deleteStack putStackSource rollbackStack listStackRevisions addApplication addEnvGroup updateStack
+// swagger:parameters getStack deleteStack putStackSource rollbackStack listStackRevisions addApplication addEnvGroup
 type stackPathParams struct {
 	// The project id
 	// in: path
@@ -820,61 +820,5 @@ func getV1StackRoutes(
 		Router:   r,
 	})
 
-	// PATCH /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/stacks/{stack_id} -> stack.NewStackUpdateStackHandler
-	// swagger:operation PATCH /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/stacks/{stack_id} updateStack
-	//
-	// Updates a stack. Currently the only value available to update is the stack name.
-	//
-	// ---
-	// produces:
-	// - application/json
-	// summary: Update Stack
-	// tags:
-	// - Stacks
-	// parameters:
-	//   - name: project_id
-	//   - name: cluster_id
-	//   - name: namespace
-	//   - name: stack_id
-	//   - in: body
-	//     name: UpdateStack
-	//     description: The stack to update
-	//     schema:
-	//       $ref: '#/definitions/UpdateStackRequest'
-	// responses:
-	//   '200':
-	//     description: Successfully updated the stack
-	//   '403':
-	//     description: Forbidden
-	updateStackEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbUpdate,
-			Method: types.HTTPVerbPatch,
-			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath + "/{stack_id}",
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.ClusterScope,
-				types.NamespaceScope,
-				types.StackScope,
-			},
-		},
-	)
-
-	updateStackHandler := stack.NewStackUpdateStackHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &router.Route{
-		Endpoint: updateStackEndpoint,
-		Handler:  updateStackHandler,
-		Router:   r,
-	})
-
 	return routes, newPath
 }

+ 0 - 10
api/types/stacks.go

@@ -63,11 +63,6 @@ type CreateStackAppResourceRequest struct {
 	SourceConfigName string `json:"source_config_name" form:"required"`
 }
 
-// swagger:model
-type UpdateStackRequest struct {
-	Name string `json:"name" form:"required"`
-}
-
 // swagger:model
 type Stack struct {
 	// The time that the stack was initially created
@@ -226,9 +221,6 @@ type StackSourceConfig struct {
 
 	// If this field is empty, the resource is deployed directly from the image repo uri
 	StackSourceConfigBuild *StackSourceConfigBuild `json:"build,omitempty"`
-
-	// Unique ID to identify between revisions
-	StableSourceConfigID string `json:"stable_source_config_id"`
 }
 
 // swagger:model
@@ -262,8 +254,6 @@ type CreateStackSourceConfigRequest struct {
 	// required: true
 	ImageTag string `json:"image_tag" form:"required"`
 
-	StableSourceConfigID string `json:"source_config_id,omitempty"`
-
 	// If this field is empty, the resource is deployed directly from the image repo uri
 	StackSourceConfigBuild *StackSourceConfigBuild `json:"build,omitempty"`
 }

+ 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 {

+ 8 - 0
dashboard/src/components/porter-form/FormDebugger.tsx

@@ -11,6 +11,7 @@ import "ace-builds/src-noconflict/mode-text";
 
 import Heading from "../form-components/Heading";
 import Helper from "../form-components/Helper";
+import { ChartType } from "shared/types";
 
 type PropsType = {
   goBack: () => void;
@@ -170,6 +171,13 @@ export default class FormDebugger extends Component<PropsType, StateType> {
           rightTabOptions={this.state.showBonusTabs ? tabOptions : []}
           renderTabContents={this.renderTabContents}
           saveButtonText={"Test Submit"}
+          injectedProps={{
+            "url-link": {
+              chart: {
+                name: "something",
+              } as ChartType,
+            },
+          }}
         />
       </StyledFormDebugger>
     );

+ 4 - 0
dashboard/src/components/porter-form/PorterForm.tsx

@@ -12,6 +12,7 @@ import {
   SelectField,
   ServiceIPListField,
   TextAreaField,
+  UrlLinkField,
 } from "./types";
 import TabRegion, { TabOption } from "../TabRegion";
 import Heading from "../form-components/Heading";
@@ -29,6 +30,7 @@ import ResourceList from "./field-components/ResourceList";
 import VeleroForm from "./field-components/VeleroForm";
 import CronInput from "./field-components/CronInput";
 import TextAreaInput from "./field-components/TextAreaInput";
+import UrlLink from "./field-components/UrlLink";
 
 interface Props {
   leftTabOptions?: TabOption[];
@@ -98,6 +100,8 @@ const PorterForm: React.FC<Props> = (props) => {
         return <CronInput {...(bundledProps as CronField)} />;
       case "text-area":
         return <TextAreaInput {...(bundledProps as TextAreaField)} />;
+      case "url-link":
+        return <UrlLink {...(bundledProps as UrlLinkField)} />;
     }
     return <p>Not Implemented: {(field as any).type}</p>;
   };

+ 94 - 0
dashboard/src/components/porter-form/field-components/UrlLink.tsx

@@ -0,0 +1,94 @@
+import { get } from "lodash";
+import React from "react";
+import styled from "styled-components";
+import { UrlLinkField } from "../types";
+import { hasSetValue } from "../utils";
+
+const populate = (str: string, obj: unknown) => {
+  return str.replace(/{[^{}]*}+/g, (match) => {
+    const key = match.replace("{", "").replace("}", "");
+    let value;
+    if (key[0] === ".") {
+      value = get(obj, key.substring(1));
+    } else {
+      value = get(obj, key);
+    }
+
+    if (typeof value !== "string") {
+      return "Couldn't find value " + key;
+    }
+
+    return value;
+  });
+};
+
+const UrlLink = (props: UrlLinkField) => {
+  const { value, label, injectedProps } = props;
+
+  if (!hasSetValue(props)) {
+    return null;
+  }
+
+  let val = value;
+
+  if (Array.isArray(value)) {
+    val = value[0];
+  }
+
+  if (typeof val !== "string") {
+    return null;
+  }
+
+  if (!injectedProps?.chart) {
+    return null;
+  }
+
+  const populatedUrl = populate(val, injectedProps.chart);
+
+  return (
+    <>
+      <Label>{label}</Label>
+      <StyledServiceRow>
+        <a href={populatedUrl} target="_blank">
+          <i className="material-icons-outlined">link</i>
+          {populatedUrl}
+        </a>
+      </StyledServiceRow>
+    </>
+  );
+};
+
+export default UrlLink;
+
+const StyledServiceRow = styled.div`
+  width: 100%;
+  height: 40px;
+  background: #ffffff11;
+  margin-bottom: 15px;
+  border-radius: 5px;
+  padding: 15px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  > a {
+    margin-left: 2px;
+    font-size: 13px;
+    user-select: text;
+    display: flex;
+    -webkit-box-align: center;
+    align-items: center;
+    > i {
+      font-size: 15px;
+      margin-right: 10px;
+    }
+  }
+`;
+
+const Label = styled.div`
+  color: #ffffff;
+  margin-bottom: 10px;
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  font-family: "Work Sans", sans-serif;
+`;

+ 11 - 2
dashboard/src/components/porter-form/types.ts

@@ -5,7 +5,7 @@
 
 // YAML Field interfaces
 
-import { ContextProps } from "../../shared/types";
+import { ChartType, ContextProps } from "../../shared/types";
 
 export interface GenericField {
   id: string;
@@ -146,6 +146,14 @@ export interface TextAreaField extends GenericInputField {
   };
 }
 
+export interface UrlLinkField extends GenericInputField {
+  type: "url-link";
+  label: string;
+  injectedProps: {
+    chart: ChartType;
+  };
+}
+
 export type FormField =
   | HeadingField
   | SubtitleField
@@ -159,7 +167,8 @@ export type FormField =
   | VeleroBackupField
   | VariableField
   | CronField
-  | TextAreaField;
+  | TextAreaField
+  | UrlLinkField;
 
 export interface ShowIfAnd {
   and: ShowIf[];

+ 3 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -870,6 +870,9 @@ const ExpandedChart: React.FC<Props> = (props) => {
                                 ? stackEnvGroups
                                 : undefined,
                           },
+                          "url-link": {
+                            chart: currentChart,
+                          },
                         }}
                       />
                     </BodyWrapper>

+ 3 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -395,6 +395,9 @@ export const ExpandedJobChartFC: React.FC<{
                   availableSyncEnvGroups:
                     isStack && !disableForm ? stackEnvGroups : undefined,
                 },
+                "url-link": {
+                  chart: chart,
+                },
               }}
             />
           )}

+ 1 - 5
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/ExpandedStack.tsx

@@ -218,11 +218,7 @@ const ExpandedStack = () => {
             component: (
               <>
                 <Gap></Gap>
-                <Settings
-                  stack={stack}
-                  onDelete={handleDelete}
-                  onUpdate={refreshStack}
-                />
+                <Settings stackName={stack.name} onDelete={handleDelete} />
               </>
             ),
           },

+ 72 - 111
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_SourceConfig.tsx

@@ -1,9 +1,11 @@
+import { Tooltip } from "@material-ui/core";
+import ImageSelector from "components/image-selector/ImageSelector";
 import SaveButton from "components/SaveButton";
-import React, { useContext, useReducer, useRef, useState } from "react";
+import React, { useContext, useMemo, useState } from "react";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import styled from "styled-components";
-import { FullStackRevision, SourceConfig } from "../types";
+import { AppResource, FullStackRevision, SourceConfig, Stack } from "../types";
 import SourceEditorDocker from "./components/SourceEditorDocker";
 
 const _SourceConfig = ({
@@ -62,13 +64,39 @@ const _SourceConfig = ({
   return (
     <SourceConfigStyles.Wrapper>
       {revision.source_configs.map((sourceConfig) => {
+        const apps = getAppsFromSourceConfig(revision.resources, sourceConfig);
+
+        const appList = formatAppList(apps, 2);
         return (
-          <SourceConfigItem
-            sourceConfig={sourceConfig}
-            key={sourceConfig.id}
-            handleChange={handleChange}
-            disabled={readOnly || buttonStatus === "loading"}
-          />
+          <SourceConfigStyles.ItemContainer>
+            {appList.hiddenApps?.length ? (
+              <Tooltip
+                title={
+                  <>
+                    {appList.hiddenApps.map((appName) => (
+                      <SourceConfigStyles.TooltipItem>
+                        {appName}
+                      </SourceConfigStyles.TooltipItem>
+                    ))}
+                  </>
+                }
+                placement={"bottom-end"}
+              >
+                <SourceConfigStyles.ItemTitle>
+                  Used by {appList.value}
+                </SourceConfigStyles.ItemTitle>
+              </Tooltip>
+            ) : (
+              <SourceConfigStyles.ItemTitle>
+                Used by {appList.value}
+              </SourceConfigStyles.ItemTitle>
+            )}
+            <SourceEditorDocker
+              sourceConfig={sourceConfig}
+              onChange={handleChange}
+              readOnly={readOnly || buttonStatus === "loading"}
+            />
+          </SourceConfigStyles.ItemContainer>
         );
       })}
       {readOnly ? null : (
@@ -89,6 +117,41 @@ const _SourceConfig = ({
 
 export default _SourceConfig;
 
+const getAppsFromSourceConfig = (
+  apps: AppResource[],
+  sourceConfig: SourceConfig
+) => {
+  return apps.filter((app) => {
+    return app.stack_source_config.id === sourceConfig.id;
+  });
+};
+
+const formatAppList = (apps: AppResource[], limit: number = 3) => {
+  if (apps.length <= limit) {
+    const formatter = new Intl.ListFormat("en", {
+      style: "long",
+      type: "conjunction",
+    });
+    return {
+      value: formatter.format(apps.map((app) => app.name)),
+      hiddenApps: [],
+    };
+  }
+
+  const hiddenApps = [...apps]
+    .splice(limit, apps.length)
+    .map((app) => app.name);
+
+  return {
+    value: apps
+      .map((app) => app.name)
+      .splice(0, limit)
+      .join(", ")
+      .concat(` and ${apps.length - limit} more`),
+    hiddenApps,
+  };
+};
+
 const SourceConfigStyles = {
   Wrapper: styled.div`
     margin-top: 30px;
@@ -101,17 +164,8 @@ const SourceConfigStyles = {
   `,
   ItemTitle: styled.div`
     font-size: 16px;
+    width: fit-content;
     font-weight: 500;
-
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    gap: 10px;
-    > span {
-      overflow-x: hidden;
-      text-overflow: ellipsis;
-      white-space: nowrap;
-    }
   `,
   TooltipItem: styled.div`
     font-size: 14px;
@@ -125,96 +179,3 @@ const SourceConfigStyles = {
     z-index: unset;
   `,
 };
-
-const SourceConfigItem = ({
-  sourceConfig,
-  handleChange,
-  disabled,
-}: {
-  sourceConfig: SourceConfig;
-  handleChange: (sourceConfig: SourceConfig) => void;
-  disabled: boolean;
-}) => {
-  const [editNameMode, toggleEditNameMode] = useReducer((prev) => !prev, false);
-  const prevName = useRef(sourceConfig.name);
-  const [name, setName] = useState(sourceConfig.name);
-
-  const handleNameChange = (newName: string) => {
-    setName(newName);
-    handleChange({ ...sourceConfig, name: newName });
-  };
-
-  const handleNameChangeCancel = () => {
-    setName(prevName.current);
-    handleChange({ ...sourceConfig, name: prevName.current });
-    toggleEditNameMode();
-  };
-
-  return (
-    <SourceConfigStyles.ItemContainer>
-      {editNameMode && !disabled ? (
-        <>
-          <SourceConfigStyles.ItemTitle>
-            <PlainTextInput
-              value={name}
-              onChange={(e) => handleNameChange(e.target.value)}
-              type="text"
-              disabled={disabled}
-            />
-            <EditButton onClick={handleNameChangeCancel}>
-              <i className="material-icons-outlined">close</i>
-            </EditButton>
-          </SourceConfigStyles.ItemTitle>
-        </>
-      ) : (
-        <SourceConfigStyles.ItemTitle>
-          <span>{name}</span>
-
-          {sourceConfig.stable_source_config_id && (
-            <EditButton
-              onClick={toggleEditNameMode}
-              disabled={!sourceConfig.stable_source_config_id}
-            >
-              <i className="material-icons-outlined">edit</i>
-            </EditButton>
-          )}
-        </SourceConfigStyles.ItemTitle>
-      )}
-
-      <SourceEditorDocker
-        sourceConfig={sourceConfig}
-        onChange={handleChange}
-        readOnly={disabled}
-      />
-    </SourceConfigStyles.ItemContainer>
-  );
-};
-
-const EditButton = styled.button`
-  outline: none;
-  cursor: pointer;
-  color: white;
-  border: 1px solid rgba(255, 255, 255, 0.333);
-  background: rgba(255, 255, 255, 0.067);
-  height: 35px;
-  width: 35px;
-  border-radius: 24px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  > i {
-    font-size: 20px;
-  }
-`;
-
-const PlainTextInput = styled.input`
-  outline: none;
-  border: 1px solid #ffffff55;
-  border-radius: 3px;
-  font-size: 13px;
-  background: #ffffff11;
-  width: 100%;
-  color: white;
-  padding: 5px 10px;
-  height: 35px;
-`;

+ 5 - 67
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Settings.tsx

@@ -1,30 +1,16 @@
 import Heading from "components/form-components/Heading";
-import Helper from "components/form-components/Helper";
-import InputRow from "components/form-components/InputRow";
-import React, { useContext, useState } from "react";
-import api from "shared/api";
+import React, { useContext } from "react";
 import { Context } from "shared/Context";
 import styled from "styled-components";
-import { SubmitButton } from "../../launch/components/styles";
-import { Stack } from "../../types";
 
 const Settings = ({
-  stack,
+  stackName,
   onDelete,
-  onUpdate,
 }: {
-  stack: Stack;
+  stackName: string;
   onDelete: () => void;
-  onUpdate: () => Promise<void>;
 }) => {
-  const {
-    currentCluster,
-    currentProject,
-    setCurrentOverlay,
-    setCurrentError,
-  } = useContext(Context);
-  const [stackName, setStackName] = useState(stack.name);
-  const [buttonStatus, setButtonStatus] = useState("");
+  const { setCurrentOverlay } = useContext(Context);
 
   const handleDelete = () => {
     setCurrentOverlay({
@@ -36,54 +22,10 @@ const Settings = ({
       onNo: () => setCurrentOverlay(null),
     });
   };
-
-  const handleStackNameChange = async () => {
-    setButtonStatus("loading");
-    try {
-      await api.updateStack(
-        "<token>",
-        {
-          name: stackName,
-        },
-        {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-          stack_id: stack.id,
-          namespace: stack.namespace,
-        }
-      );
-      await onUpdate();
-      setButtonStatus("successful");
-    } catch (err) {
-      setCurrentError(err);
-      setButtonStatus("Couldn't update the stack name. Try again later.");
-    }
-  };
-
   return (
     <Wrapper>
       <StyledSettingsSection>
-        <Heading>Update Stack name</Heading>
-
-        <InputRow
-          label="Stack name"
-          value={stackName}
-          setValue={setStackName as any}
-          type="text"
-          width="300px"
-        />
-        <SaveButton
-          text="Update"
-          onClick={handleStackNameChange}
-          disabled={stackName === stack.name}
-          makeFlush
-          clearPosition
-          statusPosition="right"
-          status={buttonStatus}
-        ></SaveButton>
-
-        <Heading>Additional Settings</Heading>
-
+        <Heading>Settings</Heading>
         <Button color="#b91133" onClick={handleDelete}>
           Delete stack
         </Button>
@@ -94,10 +36,6 @@ const Settings = ({
 
 export default Settings;
 
-const SaveButton = styled(SubmitButton)`
-  justify-content: flex-start;
-`;
-
 const Wrapper = styled.div`
   width: 100%;
   padding-bottom: 65px;

+ 3 - 21
dashboard/src/main/home/cluster-dashboard/stacks/launch/SelectSource.tsx

@@ -8,11 +8,10 @@ import Helper from "components/form-components/Helper";
 import Heading from "components/form-components/Heading";
 import styled from "styled-components";
 import TitleSection from "components/TitleSection";
-import InputRow from "components/form-components/InputRow";
 
 const SelectSource = () => {
   const { addSourceConfig } = useContext(StacksLaunchContext);
-  const [sourceName, setSourceName] = useState("");
+
   const [imageUrl, setImageUrl] = useState("");
   const [imageTag, setImageTag] = useState("");
   const { pushFiltered } = useRouting();
@@ -22,8 +21,7 @@ const SelectSource = () => {
       return;
     }
 
-    const newSource: CreateStackBody["source_configs"][0] = {
-      name: sourceName,
+    const newSource: Omit<CreateStackBody["source_configs"][0], "name"> = {
       image_repo_uri: imageUrl,
       image_tag: imageTag,
     };
@@ -41,23 +39,11 @@ const SelectSource = () => {
         New Application Stack
       </TitleSection>
       <Heading>Stack Source</Heading>
-
-      <Br />
-      <InputRowWrapper>
-        <InputRow
-          label="Source Name"
-          value={sourceName}
-          setValue={(val) => setSourceName(val as string)}
-          type="text"
-          width="100%"
-          placeholder="Leave empty for auto-generated source config name"
-        />
-      </InputRowWrapper>
-
       <Helper>
         Specify a source to deploy all stack applications from:
         <Required>*</Required>
       </Helper>
+      <Br />
       <ImageSelector
         selectedImageUrl={imageUrl}
         setSelectedImageUrl={setImageUrl}
@@ -100,7 +86,3 @@ const Polymer = styled.div`
     margin-right: 18px;
   }
 `;
-
-const InputRowWrapper = styled.div`
-  width: 60%;
-`;

+ 7 - 5
dashboard/src/main/home/cluster-dashboard/stacks/launch/Store.tsx

@@ -11,7 +11,9 @@ export type StacksLaunchContextType = {
   setStackName: (name: string) => void;
   setStackNamespace: (namespace: string) => void;
 
-  addSourceConfig: (sourceConfig: CreateStackBody["source_configs"][0]) => void;
+  addSourceConfig: (
+    sourceConfig: Omit<CreateStackBody["source_configs"][0], "name">
+  ) => void;
 
   addAppResource: (
     appResource: CreateStackBody["app_resources"][0],
@@ -40,7 +42,9 @@ const defaultValues: StacksLaunchContextType = {
   setStackName: (name: string) => {},
   setStackNamespace: (namespace: string) => {},
 
-  addSourceConfig: (sourceConfig: CreateStackBody["source_configs"][0]) => {},
+  addSourceConfig: (
+    sourceConfig: Omit<CreateStackBody["source_configs"][0], "name">
+  ) => {},
 
   addAppResource: (appResource: CreateStackBody["app_resources"][0]) => {},
 
@@ -92,9 +96,7 @@ const StacksLaunchContextProvider: React.FC<{}> = ({ children }) => {
       source_configs: [
         ...prev.source_configs,
         {
-          name:
-            sourceConfig.name ||
-            newSourceConfigName(prev.source_configs.length),
+          name: newSourceConfigName(prev.source_configs.length),
           ...sourceConfig,
         },
       ],

+ 0 - 2
dashboard/src/main/home/cluster-dashboard/stacks/types.ts

@@ -90,8 +90,6 @@ export type SourceConfig = {
   stack_id: string;
   stack_revision_id: number;
 
-  stable_source_config_id: string;
-
   build?: {
     method: "pack" | "docker";
     folder_path: string;

+ 0 - 17
dashboard/src/shared/api.tsx

@@ -2162,22 +2162,6 @@ const removeStackEnvGroup = baseApi<
     `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks/${stack_id}/remove_env_group/${env_group_name}`
 );
 
-const updateStack = baseApi<
-  {
-    name: string;
-  },
-  {
-    project_id: number;
-    cluster_id: number;
-    namespace: string;
-    stack_id: string;
-  }
->(
-  "PATCH",
-  ({ project_id, cluster_id, namespace, stack_id }) =>
-    `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks/${stack_id}`
-);
-
 const getGithubStatus = baseApi<{}, {}>("GET", ({}) => `/api/status/github`);
 
 // Bundle export to allow default api import (api.<method> is more readable)
@@ -2380,7 +2364,6 @@ export default {
   removeStackAppResource,
   addStackEnvGroup,
   removeStackEnvGroup,
-  updateStack,
 
   // STATUS
   getGithubStatus,

+ 8 - 13
internal/models/stack.go

@@ -160,10 +160,6 @@ func (s StackResource) ToStackResource(stackID string, stackRevisionID uint, sou
 type StackSourceConfig struct {
 	gorm.Model
 
-	// A unique identifier for this source config, this will allow us identify a same source config
-	// across multiple revisions and updates. This is not the same as the UID or ID which are updated over revisions.
-	StableSourceConfigID string
-
 	StackRevisionID uint
 
 	Name string
@@ -179,15 +175,14 @@ type StackSourceConfig struct {
 
 func (s StackSourceConfig) ToStackSourceConfigType(stackID string, stackRevisionID uint) *types.StackSourceConfig {
 	return &types.StackSourceConfig{
-		CreatedAt:            s.CreatedAt,
-		UpdatedAt:            s.UpdatedAt,
-		StackID:              stackID,
-		StackRevisionID:      stackRevisionID,
-		Name:                 s.Name,
-		ID:                   s.UID,
-		ImageRepoURI:         s.ImageRepoURI,
-		ImageTag:             s.ImageTag,
-		StableSourceConfigID: s.StableSourceConfigID,
+		CreatedAt:       s.CreatedAt,
+		UpdatedAt:       s.UpdatedAt,
+		StackID:         stackID,
+		StackRevisionID: stackRevisionID,
+		Name:            s.Name,
+		ID:              s.UID,
+		ImageRepoURI:    s.ImageRepoURI,
+		ImageTag:        s.ImageTag,
 	}
 }
 

+ 0 - 8
internal/repository/gorm/stack.go

@@ -118,14 +118,6 @@ func (repo *StackRepository) DeleteStack(stack *models.Stack) (*models.Stack, er
 	return stack, nil
 }
 
-func (repo *StackRepository) UpdateStack(stack *models.Stack) (*models.Stack, error) {
-	if err := repo.db.Save(stack).Error; err != nil {
-		return nil, err
-	}
-
-	return stack, nil
-}
-
 func (repo *StackRepository) UpdateStackRevision(revision *models.StackRevision) (*models.StackRevision, error) {
 	if err := repo.db.Save(revision).Error; err != nil {
 		return nil, err

+ 0 - 1
internal/repository/stack.go

@@ -10,7 +10,6 @@ type StackRepository interface {
 	ListStacks(projectID uint, clusterID uint, namespace string) ([]*models.Stack, error)
 	DeleteStack(stack *models.Stack) (*models.Stack, error)
 
-	UpdateStack(stack *models.Stack) (*models.Stack, error)
 	UpdateStackRevision(revision *models.StackRevision) (*models.StackRevision, error)
 	ReadStackRevision(stackRevisionID uint) (*models.StackRevision, error)
 	ReadStackRevisionByNumber(stackID uint, revisionNumber uint) (*models.StackRevision, error)

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

@@ -35,10 +35,6 @@ func (repo *StackRepository) DeleteStack(stack *models.Stack) (*models.Stack, er
 	panic("unimplemented")
 }
 
-func (repo *StackRepository) UpdateStack(stack *models.Stack) (*models.Stack, error) {
-	panic("unimplemented")
-}
-
 func (repo *StackRepository) UpdateStackRevision(revision *models.StackRevision) (*models.StackRevision, error) {
 	panic("unimplemented")
 }

+ 1 - 1
internal/stacks/helpers.go

@@ -52,7 +52,7 @@ func CloneAppResources(
 			if prevSourceConfig.UID == appResource.StackSourceConfigUID {
 				// find the corresponding new source config
 				for _, newSourceConfig := range newSourceConfigs {
-					if newSourceConfig.StableSourceConfigID == prevSourceConfig.StableSourceConfigID {
+					if newSourceConfig.Name == prevSourceConfig.Name {
 						linkedSourceConfigUID = newSourceConfig.UID
 					}
 				}

+ 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 {